<!DOCTYPE html>
<!--
 *  Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
-->
<html>
<head>

  <meta charset="utf-8">
  <meta name="description" content="WebRTC code samples">
  <meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
  <meta itemprop="description" content="Client-side WebRTC code samples">
  <!--<meta itemprop="image" content="../../../images/webrtc-icon-192x192.png">-->
  <meta itemprop="name" content="WebRTC code samples">
  <meta name="mobile-web-app-capable" content="yes">
  <meta id="theme-color" name="theme-color" content="#ffffff">

  <base target="_blank">

  <title>Trickle ICE</title>
  <script src="https://cdn.bootcss.com/babel-standalone/6.26.0/babel.min.js"></script>
  <!--<link rel="icon" sizes="192x192" href="../../../images/webrtc-icon-192x192.png">-->
  <!--<link href="//fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet" type="text/css">-->
  <style>

  </style>
  <!--<link rel="stylesheet" href="../../../css/main.css">-->
  <style>
    /*
 *  Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */

    @media screen and (min-width: 1000px) {
      /* hack! to detect non-touch devices */
      div#links a {
        line-height: 0.8em;
      }
    }

    audio {
      max-width: 100%;
    }

    body {
      font-family: 'Roboto', sans-serif;
      font-weight: 300;
      margin: 0;
      padding: 1em;
      word-break: break-word;
    }

    button {
      background-color: #d84a38;
      border: none;
      border-radius: 2px;
      color: white;
      font-family: 'Roboto', sans-serif;
      font-size: 0.8em;
      margin: 0 0 1em 0;
      padding: 0.5em 0.7em 0.6em 0.7em;
    }

    button:active {
      background-color: #cf402f;
    }

    button:hover {
      background-color: #cf402f;
    }

    button[disabled] {
      color: #ccc;
    }

    button[disabled]:hover {
      background-color: #d84a38;
    }

    canvas {
      background-color: #ccc;
      max-width: 100%;
      width: 100%;
    }

    code {
      font-family: 'Roboto', sans-serif;
      font-weight: 400;
    }

    div#container {
      margin: 0 auto 0 auto;
      max-width: 60em;
      padding: 1em 1.5em 1.3em 1.5em;
    }

    h1 {
      border-bottom: 1px solid #ccc;
      font-family: 'Roboto', sans-serif;
      font-weight: 500;
      margin: 0 0 0.8em 0;
      padding: 0 0 0.2em 0;
    }

    h2 {
      color: #444;
      font-weight: 500;
    }

    h3 {
      border-top: 1px solid #eee;
      color: #666;
      font-weight: 500;
      margin: 10px 0 10px 0;
      white-space: nowrap;
    }

    li {
      margin: 0 0 0.4em 0;
    }

    html {
      /* avoid annoying page width change
  when moving from the home page */
      overflow-y: scroll;
    }

    img {
      border: none;
      max-width: 100%;
    }

    input[type=radio] {
      position: relative;
      top: -1px;
    }

    p {
      color: #444;
      font-weight: 300;
    }

    section p:last-of-type {
      margin: 0;
    }

    section {
      border-bottom: 1px solid #eee;
      margin: 0 0 30px 0;
      padding: 0 0 20px 0;
    }

    section:last-of-type {
      border-bottom: none;
      padding: 0 0 1em 0;
    }

    select {
      margin: 0 1em 1em 0;
      position: relative;
      top: -1px;
    }

    h1 span {
      white-space: nowrap;
    }

    a {
      color: #6fa8dc;
      font-weight: 300;
      text-decoration: none;
    }

    h1 a {
      font-weight: 300;
      margin: 0 10px 0 0;
      white-space: nowrap;
    }

    a:hover {
      color: #3d85c6;
      text-decoration: underline;
    }

    a#viewSource {
      display: block;
      margin: 1.3em 0 0 0;
      border-top: 1px solid #999;
      padding: 1em 0 0 0;
    }

    div#errorMsg p {
      color: #ff0000;
    }

    div#links a {
      display: block;
      line-height: 1.3em;
      margin: 0 0 1.5em 0;
    }

    strong {
      font-weight: 500;
    }

    textarea {
      resize: none;
      font-family: 'Roboto', sans-serif;
    }

    video {
      background: #222;
      margin: 0 0 20px 0;
      --width: 100%;
      width: var(--width);
      height: calc(var(--width) * 0.75);
    }

    ul {
      margin: 0 0 0.5em 0;
    }

    @media screen and (max-width: 650px) {

      h1 {
        font-size: 24px;
      }
    }

    @media screen and (max-width: 550px) {
      button:active {
        background-color: darkRed;
      }

      h1 {
        font-size: 22px;
      }
    }

    @media screen and (max-width: 450px) {
      h1 {
        font-size: 20px;
      }
    }


  </style>
  <!--<link rel="stylesheet" href="css/main.css"/>-->

</head>

<body>

<div id="container">

  <h1><a href="//webrtc.github.io/samples/" title="WebRTC samples homepage">WebRTC samples</a>
    <span>Trickle ICE</span></h1>

  <section>

    <p>This page tests the trickle ICE functionality in a WebRTC implementation. It creates a PeerConnection with
      the specified ICEServers, and then starts candidate gathering for a session with a single audio stream. As
      candidates are gathered, they are displayed in the text box below, along with an indication when candidate
      gathering is complete.</p>

    <p>Note that if no getUserMedia permissions for this origin are persisted only candidates from a single
      interface will be gathered in Chrome. See <a
        href="https://tools.ietf.org/html/draft-ietf-rtcweb-ip-handling-01">the RTCWEB IP address handling
        recommendations draft</a> for details.<span id="getUserMediaPermissions">You have given permission, candidate from multiple interface will be gathered.</span>
    </p>

    <p>Individual STUN and TURN servers can be added using the Add server / Remove server controls below; in
      addition, the type of candidates released to the application can be controlled via the IceTransports
      constraint.</p>

    <p>If you test a STUN server, it works if you can gather a candidate with type "srflx".
      If you test a TURN server, it works if you can gather a candidate with type "relay".</p>
    <p>If you test just a single TURN/UDP server, this page even allows you to detect when you are using the wrong
      credential to authenticated.</p>

  </section>

  <section id="iceServers">

    <h2>ICE servers</h2>

    <select title="select-servers" id="servers" size="4">
    </select>

    <div>
      <label for="url">STUN or TURN URI:</label>
      <input id="url" value="stun:b.7mo.org:3478">
    </div>

    <div>
      <label for="username">TURN username:</label>
      <input id="username">
    </div>

    <div>
      <label for="password">TURN password:</label>
      <input id="password">
    </div>

    <div>
      <button id="add">Add Server</button>
      <button id="remove">Remove Server</button>
      <button id="reset">Reset to defaults</button>
    </div>

  </section>

  <section id="iceOptions">

    <h2>ICE options</h2>

    <div id="iceTransports">
      <label data-for="transports"><span>IceTransports value:</span></label>
      <input title="radio" type="radio" name="transports" value="all" id="all" checked>
      <span>all</span>
      <input title="radio" type="radio" name="transports" value="relay" id="relay">
      <span>relay</span>
    </div>
    <div>
      <label>ICE Candidate Pool:</label>
      <span id="poolValue">0</span>
      <span class="gray">0</span>
      <input title="iceCandidatePool" id="iceCandidatePool" type="range" min="0" max="10" value="0">
      <span class="gray">10</span>
    </div>

  </section>

  <section>

    <table id="candidates">
      <thead id="candidatesHead">
      <tr>
        <th>Time</th>
        <th>Component</th>
        <th>Type</th>
        <th>Foundation</th>
        <th>Protocol</th>
        <th>Address</th>
        <th>Port</th>
        <th>Priority</th>
      </tr>
      </thead>
      <tbody id="candidatesBody"></tbody>
    </table>
    <button id="gather">Gather candidates</button>
    <div id="error-note">Note: errors from onicecandidateerror above are not neccessarily fatal. For example an IPv6 DNS
      lookup may fail but relay candidates can still be gathered via IPv4.
    </div>
    <div id="error"></div>
  </section>


  <a href="https://github.com/webrtc/samples/tree/gh-pages/src/content/peerconnection/trickle-ice"
     title="View source for this page on GitHub" id="viewSource">View source on GitHub</a>
</div>

<!--<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>-->
<!--<script src="js/main.js" async></script>-->

<!--<script src="../../../js/lib/ga.js"></script>-->

<script title="adapter-latest.js" type="text/babel">
  (function (f) {
    if (typeof exports === "object" && typeof module !== "undefined") {
      module.exports = f()
    } else if (typeof define === "function" && define.amd) {
      define([], f)
    } else {
      let g;
      if (typeof window !== "undefined") {
        g = window
      } else if (typeof global !== "undefined") {
        g = global
      } else if (typeof self !== "undefined") {
        g = self
      } else {
        g = this
      }
      g.adapter = f()
    }
  })(function () {
    // let define, module, exports;
    return (function () {
      function r(e, n, t) {
        function o(i, f) {
          if (!n[i]) {
            if (!e[i]) {
              let c = "function" === typeof require && require;
              if (!f && c) return c(i, !0);
              if (u) return u(i, !0);
              let a = new Error("Cannot find module '" + i + "'");
              throw a.code = "MODULE_NOT_FOUND", a
            }
            let p = n[i] = {exports: {}};
            e[i][0].call(p.exports, function (r) {
              let n = e[i][1][r];
              return o(n || r)
            }, p, p.exports, r, e, n, t)
          }
          return n[i].exports
        }

        for (let u = "function" === typeof require && require, i = 0; i < t.length; i++) o(t[i]);
        return o
      }

      return r
    })()({
      1: [function (require, module, exports) {
        /*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
        /* eslint-env node */

        'use strict';

        // var _adapter_factory = require('./adapter_factory.js');
        let adapter;
        let _adapter_factory = require('./adapter_factory.js');

        // var adapter = (0, _adapter_factory.adapterFactory)({window: window});
        adapter = (0, _adapter_factory.adapterFactory)({window: window});
        module.exports = adapter; // this is the difference from adapter_core.

      }, {"./adapter_factory.js": 2}], 2: [function (require, module, exports) {
        'use strict';

        Object.defineProperty(exports, "__esModule", {
          value: true
        });
        exports.adapterFactory = adapterFactory;

        let _utils = require('./utils');

        let utils = _interopRequireWildcard(_utils);

        let _chrome_shim = require('./chrome/chrome_shim');

        let chromeShim = _interopRequireWildcard(_chrome_shim);

        let _edge_shim = require('./edge/edge_shim');

        let edgeShim = _interopRequireWildcard(_edge_shim);

        let _firefox_shim = require('./firefox/firefox_shim');

        let firefoxShim = _interopRequireWildcard(_firefox_shim);

        let _safari_shim = require('./safari/safari_shim');

        let safariShim = _interopRequireWildcard(_safari_shim);

        let _common_shim = require('./common_shim');

        let commonShim = _interopRequireWildcard(_common_shim);

        function _interopRequireWildcard(obj) {
          if (obj && obj.__esModule) {
            return obj;
          } else {
            let newObj = {};
            if (obj != null) {
              for (let key in obj) {
                if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key];
              }
            }
            newObj.default = obj;
            return newObj;
          }
        }

// Shimming starts here.
        /*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
        function adapterFactory() {
          let _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
            window = _ref.window;

          let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
            shimChrome: true,
            shimFirefox: true,
            shimEdge: true,
            shimSafari: true
          };

          // Utils.
          let logging = utils.log;
          let browserDetails = utils.detectBrowser(window);

          let adapter = {
            browserDetails: browserDetails,
            commonShim: commonShim,
            extractVersion: utils.extractVersion,
            disableLog: utils.disableLog,
            disableWarnings: utils.disableWarnings
          };

          // Shim browser if found.
          switch (browserDetails.browser) {
            case 'chrome':
              if (!chromeShim || !chromeShim.shimPeerConnection || !options.shimChrome) {
                logging('Chrome shim is not included in this adapter release.');
                return adapter;
              }
              if (browserDetails.version === null) {
                logging('Chrome shim can not determine version, not shimming.');
                return adapter;
              }
              logging('adapter.js shimming chrome.');
              // Export to the adapter global object visible in the browser.
              adapter.browserShim = chromeShim;

              chromeShim.shimGetUserMedia(window);
              chromeShim.shimMediaStream(window);
              chromeShim.shimPeerConnection(window);
              chromeShim.shimOnTrack(window);
              chromeShim.shimAddTrackRemoveTrack(window);
              chromeShim.shimGetSendersWithDtmf(window);
              chromeShim.shimGetStats(window);
              chromeShim.shimSenderReceiverGetStats(window);
              chromeShim.fixNegotiationNeeded(window);

              commonShim.shimRTCIceCandidate(window);
              commonShim.shimConnectionState(window);
              commonShim.shimMaxMessageSize(window);
              commonShim.shimSendThrowTypeError(window);
              commonShim.removeAllowExtmapMixed(window);
              break;
            case 'firefox':
              if (!firefoxShim || !firefoxShim.shimPeerConnection || !options.shimFirefox) {
                logging('Firefox shim is not included in this adapter release.');
                return adapter;
              }
              logging('adapter.js shimming firefox.');
              // Export to the adapter global object visible in the browser.
              adapter.browserShim = firefoxShim;

              firefoxShim.shimGetUserMedia(window);
              firefoxShim.shimPeerConnection(window);
              firefoxShim.shimOnTrack(window);
              firefoxShim.shimRemoveStream(window);
              firefoxShim.shimSenderGetStats(window);
              firefoxShim.shimReceiverGetStats(window);
              firefoxShim.shimRTCDataChannel(window);
              firefoxShim.shimAddTransceiver(window);
              firefoxShim.shimGetParameters(window);
              firefoxShim.shimCreateOffer(window);
              firefoxShim.shimCreateAnswer(window);

              commonShim.shimRTCIceCandidate(window);
              commonShim.shimConnectionState(window);
              commonShim.shimMaxMessageSize(window);
              commonShim.shimSendThrowTypeError(window);
              break;
            case 'edge':
              if (!edgeShim || !edgeShim.shimPeerConnection || !options.shimEdge) {
                logging('MS edge shim is not included in this adapter release.');
                return adapter;
              }
              logging('adapter.js shimming edge.');
              // Export to the adapter global object visible in the browser.
              adapter.browserShim = edgeShim;

              edgeShim.shimGetUserMedia(window);
              edgeShim.shimGetDisplayMedia(window);
              edgeShim.shimPeerConnection(window);
              edgeShim.shimReplaceTrack(window);

              // the edge shim implements the full RTCIceCandidate object.

              commonShim.shimMaxMessageSize(window);
              commonShim.shimSendThrowTypeError(window);
              break;
            case 'safari':
              if (!safariShim || !options.shimSafari) {
                logging('Safari shim is not included in this adapter release.');
                return adapter;
              }
              logging('adapter.js shimming safari.');
              // Export to the adapter global object visible in the browser.
              adapter.browserShim = safariShim;

              safariShim.shimRTCIceServerUrls(window);
              safariShim.shimCreateOfferLegacy(window);
              safariShim.shimCallbacksAPI(window);
              safariShim.shimLocalStreamsAPI(window);
              safariShim.shimRemoteStreamsAPI(window);
              safariShim.shimTrackEventTransceiver(window);
              safariShim.shimGetUserMedia(window);
              safariShim.shimAudioContext(window);

              commonShim.shimRTCIceCandidate(window);
              commonShim.shimMaxMessageSize(window);
              commonShim.shimSendThrowTypeError(window);
              commonShim.removeAllowExtmapMixed(window);
              break;
            default:
              logging('Unsupported browser!');
              break;
          }

          return adapter;
        }

// Browser shims.

      }, {
        "./chrome/chrome_shim": 3,
        "./common_shim": 6,
        "./edge/edge_shim": 7,
        "./firefox/firefox_shim": 11,
        "./safari/safari_shim": 14,
        "./utils": 15
      }], 3: [function (require, module, exports) {
        /*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
        /* eslint-env node */
        'use strict';

        Object.defineProperty(exports, "__esModule", {
          value: true
        });
        exports.shimGetDisplayMedia = exports.shimGetUserMedia = undefined;

        let _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
          return typeof obj;
        } : function (obj) {
          return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
        };

        let _getusermedia = require('./getusermedia');

        Object.defineProperty(exports, 'shimGetUserMedia', {
          enumerable: true,
          get: function get() {
            return _getusermedia.shimGetUserMedia;
          }
        });

        let _getdisplaymedia = require('./getdisplaymedia');

        Object.defineProperty(exports, 'shimGetDisplayMedia', {
          enumerable: true,
          get: function get() {
            return _getdisplaymedia.shimGetDisplayMedia;
          }
        });
        exports.shimMediaStream = shimMediaStream;
        exports.shimOnTrack = shimOnTrack;
        exports.shimGetSendersWithDtmf = shimGetSendersWithDtmf;
        exports.shimGetStats = shimGetStats;
        exports.shimSenderReceiverGetStats = shimSenderReceiverGetStats;
        exports.shimAddTrackRemoveTrackWithNative = shimAddTrackRemoveTrackWithNative;
        exports.shimAddTrackRemoveTrack = shimAddTrackRemoveTrack;
        exports.shimPeerConnection = shimPeerConnection;
        exports.fixNegotiationNeeded = fixNegotiationNeeded;

        let _utils = require('../utils.js');

        let utils = _interopRequireWildcard(_utils);

        function _interopRequireWildcard(obj) {
          if (obj && obj.__esModule) {
            return obj;
          } else {
            let newObj = {};
            if (obj != null) {
              for (let key in obj) {
                if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key];
              }
            }
            newObj.default = obj;
            return newObj;
          }
        }

        function _defineProperty(obj, key, value) {
          if (key in obj) {
            Object.defineProperty(obj, key, {value: value, enumerable: true, configurable: true, writable: true});
          } else {
            obj[key] = value;
          }
          return obj;
        }

        function shimMediaStream(window) {
          window.MediaStream = window.MediaStream || window.webkitMediaStream;
        }

        function shimOnTrack(window) {
          if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCPeerConnection && !('ontrack' in window.RTCPeerConnection.prototype)) {
            Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
              get: function get() {
                return this._ontrack;
              },
              set: function set(f) {
                if (this._ontrack) {
                  this.removeEventListener('track', this._ontrack);
                }
                this.addEventListener('track', this._ontrack = f);
              },

              enumerable: true,
              configurable: true
            });
            let origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription;
            window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription() {
              let _this = this;

              if (!this._ontrackpoly) {
                this._ontrackpoly = function (e) {
                  // onaddstream does not fire when a track is added to an existing
                  // stream. But stream.onaddtrack is implemented so we use that.
                  e.stream.addEventListener('addtrack', function (te) {
                    let receiver = void 0;
                    if (window.RTCPeerConnection.prototype.getReceivers) {
                      receiver = _this.getReceivers().find(function (r) {
                        return r.track && r.track.id === te.track.id;
                      });
                    } else {
                      receiver = {track: te.track};
                    }

                    let event = new Event('track');
                    event.track = te.track;
                    event.receiver = receiver;
                    event.transceiver = {receiver: receiver};
                    event.streams = [e.stream];
                    _this.dispatchEvent(event);
                  });
                  e.stream.getTracks().forEach(function (track) {
                    let receiver = void 0;
                    if (window.RTCPeerConnection.prototype.getReceivers) {
                      receiver = _this.getReceivers().find(function (r) {
                        return r.track && r.track.id === track.id;
                      });
                    } else {
                      receiver = {track: track};
                    }
                    let event = new Event('track');
                    event.track = track;
                    event.receiver = receiver;
                    event.transceiver = {receiver: receiver};
                    event.streams = [e.stream];
                    _this.dispatchEvent(event);
                  });
                };
                this.addEventListener('addstream', this._ontrackpoly);
              }
              return origSetRemoteDescription.apply(this, arguments);
            };
          } else {
            // even if RTCRtpTransceiver is in window, it is only used and
            // emitted in unified-plan. Unfortunately this means we need
            // to unconditionally wrap the event.
            utils.wrapPeerConnectionEvent(window, 'track', function (e) {
              if (!e.transceiver) {
                Object.defineProperty(e, 'transceiver', {value: {receiver: e.receiver}});
              }
              return e;
            });
          }
        }

        function shimGetSendersWithDtmf(window) {
          // Overrides addTrack/removeTrack, depends on shimAddTrackRemoveTrack.
          if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCPeerConnection && !('getSenders' in window.RTCPeerConnection.prototype) && 'createDTMFSender' in window.RTCPeerConnection.prototype) {
            let shimSenderWithDtmf = function shimSenderWithDtmf(pc, track) {
              return {
                track: track,
                get dtmf() {
                  if (this._dtmf === undefined) {
                    if (track.kind === 'audio') {
                      this._dtmf = pc.createDTMFSender(track);
                    } else {
                      this._dtmf = null;
                    }
                  }
                  return this._dtmf;
                },
                _pc: pc
              };
            };

            // augment addTrack when getSenders is not available.
            if (!window.RTCPeerConnection.prototype.getSenders) {
              window.RTCPeerConnection.prototype.getSenders = function getSenders() {
                this._senders = this._senders || [];
                return this._senders.slice(); // return a copy of the internal state.
              };
              let origAddTrack = window.RTCPeerConnection.prototype.addTrack;
              window.RTCPeerConnection.prototype.addTrack = function addTrack(track, stream) {
                let sender = origAddTrack.apply(this, arguments);
                if (!sender) {
                  sender = shimSenderWithDtmf(this, track);
                  this._senders.push(sender);
                }
                return sender;
              };

              let origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack;
              window.RTCPeerConnection.prototype.removeTrack = function removeTrack(sender) {
                origRemoveTrack.apply(this, arguments);
                let idx = this._senders.indexOf(sender);
                if (idx !== -1) {
                  this._senders.splice(idx, 1);
                }
              };
            }
            let origAddStream = window.RTCPeerConnection.prototype.addStream;
            window.RTCPeerConnection.prototype.addStream = function addStream(stream) {
              let _this2 = this;

              this._senders = this._senders || [];
              origAddStream.apply(this, [stream]);
              stream.getTracks().forEach(function (track) {
                _this2._senders.push(shimSenderWithDtmf(_this2, track));
              });
            };

            let origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
            window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) {
              let _this3 = this;

              this._senders = this._senders || [];
              origRemoveStream.apply(this, [stream]);

              stream.getTracks().forEach(function (track) {
                let sender = _this3._senders.find(function (s) {
                  return s.track === track;
                });
                if (sender) {
                  // remove sender
                  _this3._senders.splice(_this3._senders.indexOf(sender), 1);
                }
              });
            };
          } else if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCPeerConnection && 'getSenders' in window.RTCPeerConnection.prototype && 'createDTMFSender' in window.RTCPeerConnection.prototype && window.RTCRtpSender && !('dtmf' in window.RTCRtpSender.prototype)) {
            let origGetSenders = window.RTCPeerConnection.prototype.getSenders;
            window.RTCPeerConnection.prototype.getSenders = function getSenders() {
              let _this4 = this;

              let senders = origGetSenders.apply(this, []);
              senders.forEach(function (sender) {
                return sender._pc = _this4;
              });
              return senders;
            };

            Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', {
              get: function get() {
                if (this._dtmf === undefined) {
                  if (this.track.kind === 'audio') {
                    this._dtmf = this._pc.createDTMFSender(this.track);
                  } else {
                    this._dtmf = null;
                  }
                }
                return this._dtmf;
              }
            });
          }
        }

        function shimGetStats(window) {
          if (!window.RTCPeerConnection) {
            return;
          }

          let origGetStats = window.RTCPeerConnection.prototype.getStats;
          window.RTCPeerConnection.prototype.getStats = function getStats() {
            let _this5 = this;

            let _arguments = Array.prototype.slice.call(arguments),
              selector = _arguments[0],
              onSucc = _arguments[1],
              onErr = _arguments[2];

            // If selector is a function then we are in the old style stats so just
            // pass back the original getStats format to avoid breaking old users.


            if (arguments.length > 0 && typeof selector === 'function') {
              return origGetStats.apply(this, arguments);
            }

            // When spec-style getStats is supported, return those when called with
            // either no arguments or the selector argument is null.
            if (origGetStats.length === 0 && (arguments.length === 0 || typeof selector !== 'function')) {
              return origGetStats.apply(this, []);
            }

            let fixChromeStats_ = function fixChromeStats_(response) {
              let standardReport = {};
              let reports = response.result();
              reports.forEach(function (report) {
                let standardStats = {
                  id: report.id,
                  timestamp: report.timestamp,
                  type: {
                    localcandidate: 'local-candidate',
                    remotecandidate: 'remote-candidate'
                  }[report.type] || report.type
                };
                report.names().forEach(function (name) {
                  standardStats[name] = report.stat(name);
                });
                standardReport[standardStats.id] = standardStats;
              });

              return standardReport;
            };

            // shim getStats with maplike support
            let makeMapStats = function makeMapStats(stats) {
              return new Map(Object.keys(stats).map(function (key) {
                return [key, stats[key]];
              }));
            };

            if (arguments.length >= 2) {
              let successCallbackWrapper_ = function successCallbackWrapper_(response) {
                onSucc(makeMapStats(fixChromeStats_(response)));
              };

              return origGetStats.apply(this, [successCallbackWrapper_, selector]);
            }

            // promise-support
            return new Promise(function (resolve, reject) {
              origGetStats.apply(_this5, [function (response) {
                resolve(makeMapStats(fixChromeStats_(response)));
              }, reject]);
            }).then(onSucc, onErr);
          };
        }

        function shimSenderReceiverGetStats(window) {
          if (!((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCPeerConnection && window.RTCRtpSender && window.RTCRtpReceiver)) {
            return;
          }

          // shim sender stats.
          if (!('getStats' in window.RTCRtpSender.prototype)) {
            let origGetSenders = window.RTCPeerConnection.prototype.getSenders;
            if (origGetSenders) {
              window.RTCPeerConnection.prototype.getSenders = function getSenders() {
                let _this6 = this;

                let senders = origGetSenders.apply(this, []);
                senders.forEach(function (sender) {
                  return sender._pc = _this6;
                });
                return senders;
              };
            }

            let origAddTrack = window.RTCPeerConnection.prototype.addTrack;
            if (origAddTrack) {
              window.RTCPeerConnection.prototype.addTrack = function addTrack() {
                let sender = origAddTrack.apply(this, arguments);
                sender._pc = this;
                return sender;
              };
            }
            window.RTCRtpSender.prototype.getStats = function getStats() {
              let sender = this;
              return this._pc.getStats().then(function (result) {
                return (
                  /* Note: this will include stats of all senders that
           *   send a track with the same id as sender.track as
           *   it is not possible to identify the RTCRtpSender.
           */
                  utils.filterStats(result, sender.track, true)
                );
              });
            };
          }

          // shim receiver stats.
          if (!('getStats' in window.RTCRtpReceiver.prototype)) {
            let origGetReceivers = window.RTCPeerConnection.prototype.getReceivers;
            if (origGetReceivers) {
              window.RTCPeerConnection.prototype.getReceivers = function getReceivers() {
                let _this7 = this;

                let receivers = origGetReceivers.apply(this, []);
                receivers.forEach(function (receiver) {
                  return receiver._pc = _this7;
                });
                return receivers;
              };
            }
            utils.wrapPeerConnectionEvent(window, 'track', function (e) {
              e.receiver._pc = e.srcElement;
              return e;
            });
            window.RTCRtpReceiver.prototype.getStats = function getStats() {
              let receiver = this;
              return this._pc.getStats().then(function (result) {
                return utils.filterStats(result, receiver.track, false);
              });
            };
          }

          if (!('getStats' in window.RTCRtpSender.prototype && 'getStats' in window.RTCRtpReceiver.prototype)) {
            return;
          }

          // shim RTCPeerConnection.getStats(track).
          let origGetStats = window.RTCPeerConnection.prototype.getStats;
          window.RTCPeerConnection.prototype.getStats = function getStats() {
            if (arguments.length > 0 && arguments[0] instanceof window.MediaStreamTrack) {
              let track = arguments[0];
              let sender = void 0;
              let receiver = void 0;
              let err = void 0;
              this.getSenders().forEach(function (s) {
                if (s.track === track) {
                  if (sender) {
                    err = true;
                  } else {
                    sender = s;
                  }
                }
              });
              this.getReceivers().forEach(function (r) {
                if (r.track === track) {
                  if (receiver) {
                    err = true;
                  } else {
                    receiver = r;
                  }
                }
                return r.track === track;
              });
              if (err || sender && receiver) {
                return Promise.reject(new DOMException('There are more than one sender or receiver for the track.', 'InvalidAccessError'));
              } else if (sender) {
                return sender.getStats();
              } else if (receiver) {
                return receiver.getStats();
              }
              return Promise.reject(new DOMException('There is no sender or receiver for the track.', 'InvalidAccessError'));
            }
            return origGetStats.apply(this, arguments);
          };
        }

        function shimAddTrackRemoveTrackWithNative(window) {
          // shim addTrack/removeTrack with native variants in order to make
          // the interactions with legacy getLocalStreams behave as in other browsers.
          // Keeps a mapping stream.id => [stream, rtpsenders...]
          window.RTCPeerConnection.prototype.getLocalStreams = function getLocalStreams() {
            let _this8 = this;

            this._shimmedLocalStreams = this._shimmedLocalStreams || {};
            return Object.keys(this._shimmedLocalStreams).map(function (streamId) {
              return _this8._shimmedLocalStreams[streamId][0];
            });
          };

          let origAddTrack = window.RTCPeerConnection.prototype.addTrack;
          window.RTCPeerConnection.prototype.addTrack = function addTrack(track, stream) {
            if (!stream) {
              return origAddTrack.apply(this, arguments);
            }
            this._shimmedLocalStreams = this._shimmedLocalStreams || {};

            let sender = origAddTrack.apply(this, arguments);
            if (!this._shimmedLocalStreams[stream.id]) {
              this._shimmedLocalStreams[stream.id] = [stream, sender];
            } else if (this._shimmedLocalStreams[stream.id].indexOf(sender) === -1) {
              this._shimmedLocalStreams[stream.id].push(sender);
            }
            return sender;
          };

          let origAddStream = window.RTCPeerConnection.prototype.addStream;
          window.RTCPeerConnection.prototype.addStream = function addStream(stream) {
            let _this9 = this;

            this._shimmedLocalStreams = this._shimmedLocalStreams || {};

            stream.getTracks().forEach(function (track) {
              let alreadyExists = _this9.getSenders().find(function (s) {
                return s.track === track;
              });
              if (alreadyExists) {
                throw new DOMException('Track already exists.', 'InvalidAccessError');
              }
            });
            let existingSenders = this.getSenders();
            origAddStream.apply(this, arguments);
            let newSenders = this.getSenders().filter(function (newSender) {
              return existingSenders.indexOf(newSender) === -1;
            });
            this._shimmedLocalStreams[stream.id] = [stream].concat(newSenders);
          };

          let origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
          window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) {
            this._shimmedLocalStreams = this._shimmedLocalStreams || {};
            delete this._shimmedLocalStreams[stream.id];
            return origRemoveStream.apply(this, arguments);
          };

          let origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack;
          window.RTCPeerConnection.prototype.removeTrack = function removeTrack(sender) {
            let _this10 = this;

            this._shimmedLocalStreams = this._shimmedLocalStreams || {};
            if (sender) {
              Object.keys(this._shimmedLocalStreams).forEach(function (streamId) {
                let idx = _this10._shimmedLocalStreams[streamId].indexOf(sender);
                if (idx !== -1) {
                  _this10._shimmedLocalStreams[streamId].splice(idx, 1);
                }
                if (_this10._shimmedLocalStreams[streamId].length === 1) {
                  delete _this10._shimmedLocalStreams[streamId];
                }
              });
            }
            return origRemoveTrack.apply(this, arguments);
          };
        }

        function shimAddTrackRemoveTrack(window) {
          if (!window.RTCPeerConnection) {
            return;
          }
          let browserDetails = utils.detectBrowser(window);
          // shim addTrack and removeTrack.
          if (window.RTCPeerConnection.prototype.addTrack && browserDetails.version >= 65) {
            return shimAddTrackRemoveTrackWithNative(window);
          }

          // also shim pc.getLocalStreams when addTrack is shimmed
          // to return the original streams.
          let origGetLocalStreams = window.RTCPeerConnection.prototype.getLocalStreams;
          window.RTCPeerConnection.prototype.getLocalStreams = function getLocalStreams() {
            let _this11 = this;

            let nativeStreams = origGetLocalStreams.apply(this);
            this._reverseStreams = this._reverseStreams || {};
            return nativeStreams.map(function (stream) {
              return _this11._reverseStreams[stream.id];
            });
          };

          let origAddStream = window.RTCPeerConnection.prototype.addStream;
          window.RTCPeerConnection.prototype.addStream = function addStream(stream) {
            let _this12 = this;

            this._streams = this._streams || {};
            this._reverseStreams = this._reverseStreams || {};

            stream.getTracks().forEach(function (track) {
              let alreadyExists = _this12.getSenders().find(function (s) {
                return s.track === track;
              });
              if (alreadyExists) {
                throw new DOMException('Track already exists.', 'InvalidAccessError');
              }
            });
            // Add identity mapping for consistency with addTrack.
            // Unless this is being used with a stream from addTrack.
            if (!this._reverseStreams[stream.id]) {
              let newStream = new window.MediaStream(stream.getTracks());
              this._streams[stream.id] = newStream;
              this._reverseStreams[newStream.id] = stream;
              stream = newStream;
            }
            origAddStream.apply(this, [stream]);
          };

          let origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
          window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) {
            this._streams = this._streams || {};
            this._reverseStreams = this._reverseStreams || {};

            origRemoveStream.apply(this, [this._streams[stream.id] || stream]);
            delete this._reverseStreams[this._streams[stream.id] ? this._streams[stream.id].id : stream.id];
            delete this._streams[stream.id];
          };

          window.RTCPeerConnection.prototype.addTrack = function addTrack(track, stream) {
            let _this13 = this;

            if (this.signalingState === 'closed') {
              throw new DOMException('The RTCPeerConnection\'s signalingState is \'closed\'.', 'InvalidStateError');
            }
            let streams = [].slice.call(arguments, 1);
            if (streams.length !== 1 || !streams[0].getTracks().find(function (t) {
              return t === track;
            })) {
              // this is not fully correct but all we can manage without
              // [[associated MediaStreams]] internal slot.
              throw new DOMException('The adapter.js addTrack polyfill only supports a single ' + ' stream which is associated with the specified track.', 'NotSupportedError');
            }

            let alreadyExists = this.getSenders().find(function (s) {
              return s.track === track;
            });
            if (alreadyExists) {
              throw new DOMException('Track already exists.', 'InvalidAccessError');
            }

            this._streams = this._streams || {};
            this._reverseStreams = this._reverseStreams || {};
            let oldStream = this._streams[stream.id];
            if (oldStream) {
              // this is using odd Chrome behaviour, use with caution:
              // https://bugs.chromium.org/p/webrtc/issues/detail?id=7815
              // Note: we rely on the high-level addTrack/dtmf shim to
              // create the sender with a dtmf sender.
              oldStream.addTrack(track);

              // Trigger ONN async.
              Promise.resolve().then(function () {
                _this13.dispatchEvent(new Event('negotiationneeded'));
              });
            } else {
              let newStream = new window.MediaStream([track]);
              this._streams[stream.id] = newStream;
              this._reverseStreams[newStream.id] = stream;
              this.addStream(newStream);
            }
            return this.getSenders().find(function (s) {
              return s.track === track;
            });
          };

          // replace the internal stream id with the external one and
          // vice versa.
          function replaceInternalStreamId(pc, description) {
            let sdp = description.sdp;
            Object.keys(pc._reverseStreams || []).forEach(function (internalId) {
              let externalStream = pc._reverseStreams[internalId];
              let internalStream = pc._streams[externalStream.id];
              sdp = sdp.replace(new RegExp(internalStream.id, 'g'), externalStream.id);
            });
            return new RTCSessionDescription({
              type: description.type,
              sdp: sdp
            });
          }

          function replaceExternalStreamId(pc, description) {
            let sdp = description.sdp;
            Object.keys(pc._reverseStreams || []).forEach(function (internalId) {
              let externalStream = pc._reverseStreams[internalId];
              let internalStream = pc._streams[externalStream.id];
              sdp = sdp.replace(new RegExp(externalStream.id, 'g'), internalStream.id);
            });
            return new RTCSessionDescription({
              type: description.type,
              sdp: sdp
            });
          }

          ['createOffer', 'createAnswer'].forEach(function (method) {
            let nativeMethod = window.RTCPeerConnection.prototype[method];
            let methodObj = _defineProperty({}, method, function () {
              let _this14 = this;

              let args = arguments;
              let isLegacyCall = arguments.length && typeof arguments[0] === 'function';
              if (isLegacyCall) {
                return nativeMethod.apply(this, [function (description) {
                  let desc = replaceInternalStreamId(_this14, description);
                  args[0].apply(null, [desc]);
                }, function (err) {
                  if (args[1]) {
                    args[1].apply(null, err);
                  }
                }, arguments[2]]);
              }
              return nativeMethod.apply(this, arguments).then(function (description) {
                return replaceInternalStreamId(_this14, description);
              });
            });
            window.RTCPeerConnection.prototype[method] = methodObj[method];
          });

          let origSetLocalDescription = window.RTCPeerConnection.prototype.setLocalDescription;
          window.RTCPeerConnection.prototype.setLocalDescription = function setLocalDescription() {
            if (!arguments.length || !arguments[0].type) {
              return origSetLocalDescription.apply(this, arguments);
            }
            arguments[0] = replaceExternalStreamId(this, arguments[0]);
            return origSetLocalDescription.apply(this, arguments);
          };

          // TO DO: mangle getStats: https://w3c.github.io/webrtc-stats/#dom-rtcmediastreamstats-streamidentifier

          let origLocalDescription = Object.getOwnPropertyDescriptor(window.RTCPeerConnection.prototype, 'localDescription');
          Object.defineProperty(window.RTCPeerConnection.prototype, 'localDescription', {
            get: function get() {
              let description = origLocalDescription.get.apply(this);
              if (description.type === '') {
                return description;
              }
              return replaceInternalStreamId(this, description);
            }
          });

          window.RTCPeerConnection.prototype.removeTrack = function removeTrack(sender) {
            let _this15 = this;

            if (this.signalingState === 'closed') {
              throw new DOMException('The RTCPeerConnection\'s signalingState is \'closed\'.', 'InvalidStateError');
            }
            // We can not yet check for sender instanceof RTCRtpSender
            // since we shim RTPSender. So we check if sender._pc is set.
            if (!sender._pc) {
              throw new DOMException('Argument 1 of RTCPeerConnection.removeTrack ' + 'does not implement interface RTCRtpSender.', 'TypeError');
            }
            let isLocal = sender._pc === this;
            if (!isLocal) {
              throw new DOMException('Sender was not created by this connection.', 'InvalidAccessError');
            }

            // Search for the native stream the senders track belongs to.
            this._streams = this._streams || {};
            let stream = void 0;
            Object.keys(this._streams).forEach(function (streamid) {
              let hasTrack = _this15._streams[streamid].getTracks().find(function (track) {
                return sender.track === track;
              });
              if (hasTrack) {
                stream = _this15._streams[streamid];
              }
            });

            if (stream) {
              if (stream.getTracks().length === 1) {
                // if this is the last track of the stream, remove the stream. This
                // takes care of any shimmed _senders.
                this.removeStream(this._reverseStreams[stream.id]);
              } else {
                // relying on the same odd chrome behaviour as above.
                stream.removeTrack(sender.track);
              }
              this.dispatchEvent(new Event('negotiationneeded'));
            }
          };
        }

        function shimPeerConnection(window) {
          let browserDetails = utils.detectBrowser(window);

          if (!window.RTCPeerConnection && window.webkitRTCPeerConnection) {
            // very basic support for old versions.
            window.RTCPeerConnection = window.webkitRTCPeerConnection;
          }
          if (!window.RTCPeerConnection) {
            return;
          }

          let addIceCandidateNullSupported = window.RTCPeerConnection.prototype.addIceCandidate.length === 0;

          // shim implicit creation of RTCSessionDescription/RTCIceCandidate
          if (browserDetails.version < 53) {
            ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'].forEach(function (method) {
              let nativeMethod = window.RTCPeerConnection.prototype[method];
              let methodObj = _defineProperty({}, method, function () {
                arguments[0] = new (method === 'addIceCandidate' ? window.RTCIceCandidate : window.RTCSessionDescription)(arguments[0]);
                return nativeMethod.apply(this, arguments);
              });
              window.RTCPeerConnection.prototype[method] = methodObj[method];
            });
          }

          // support for addIceCandidate(null or undefined)
          let nativeAddIceCandidate = window.RTCPeerConnection.prototype.addIceCandidate;
          window.RTCPeerConnection.prototype.addIceCandidate = function addIceCandidate() {
            if (!addIceCandidateNullSupported && !arguments[0]) {
              if (arguments[1]) {
                arguments[1].apply(null);
              }
              return Promise.resolve();
            }
            // Firefox 68+ emits and processes {candidate: "", ...}, ignore
            // in older versions. Native support planned for Chrome M77.
            if (browserDetails.version < 78 && arguments[0] && arguments[0].candidate === '') {
              return Promise.resolve();
            }
            return nativeAddIceCandidate.apply(this, arguments);
          };
        }

// Attempt to fix ONN in plan-b mode.
        function fixNegotiationNeeded(window) {
          let browserDetails = utils.detectBrowser(window);
          utils.wrapPeerConnectionEvent(window, 'negotiationneeded', function (e) {
            let pc = e.target;
            if (browserDetails.version < 72 || pc.getConfiguration && pc.getConfiguration().sdpSemantics === 'plan-b') {
              if (pc.signalingState !== 'stable') {
                return;
              }
            }
            return e;
          });
        }

      }, {"../utils.js": 15, "./getdisplaymedia": 4, "./getusermedia": 5}], 4: [function (require, module, exports) {
        /*
 *  Copyright (c) 2018 The adapter.js project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
        /* eslint-env node */
        'use strict';

        Object.defineProperty(exports, "__esModule", {
          value: true
        });
        exports.shimGetDisplayMedia = shimGetDisplayMedia;

        function shimGetDisplayMedia(window, getSourceId) {
          if (window.navigator.mediaDevices && 'getDisplayMedia' in window.navigator.mediaDevices) {
            return;
          }
          if (!window.navigator.mediaDevices) {
            return;
          }
          // getSourceId is a function that returns a promise resolving with
          // the sourceId of the screen/window/tab to be shared.
          if (typeof getSourceId !== 'function') {
            console.error('shimGetDisplayMedia: getSourceId argument is not ' + 'a function');
            return;
          }
          window.navigator.mediaDevices.getDisplayMedia = function getDisplayMedia(constraints) {
            return getSourceId(constraints).then(function (sourceId) {
              let widthSpecified = constraints.video && constraints.video.width;
              let heightSpecified = constraints.video && constraints.video.height;
              let frameRateSpecified = constraints.video && constraints.video.frameRate;
              constraints.video = {
                mandatory: {
                  chromeMediaSource: 'desktop',
                  chromeMediaSourceId: sourceId,
                  maxFrameRate: frameRateSpecified || 3
                }
              };
              if (widthSpecified) {
                constraints.video.mandatory.maxWidth = widthSpecified;
              }
              if (heightSpecified) {
                constraints.video.mandatory.maxHeight = heightSpecified;
              }
              return window.navigator.mediaDevices.getUserMedia(constraints);
            });
          };
        }

      }, {}], 5: [function (require, module, exports) {
        /*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
        /* eslint-env node */
        'use strict';

        Object.defineProperty(exports, "__esModule", {
          value: true
        });

        let _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
          return typeof obj;
        } : function (obj) {
          return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
        };

        exports.shimGetUserMedia = shimGetUserMedia;

        let _utils = require('../utils.js');

        let utils = _interopRequireWildcard(_utils);

        function _interopRequireWildcard(obj) {
          if (obj && obj.__esModule) {
            return obj;
          } else {
            let newObj = {};
            if (obj != null) {
              for (let key in obj) {
                if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key];
              }
            }
            newObj.default = obj;
            return newObj;
          }
        }

        let logging = utils.log;

        function shimGetUserMedia(window) {
          let navigator = window && window.navigator;

          if (!navigator.mediaDevices) {
            return;
          }

          let browserDetails = utils.detectBrowser(window);

          let constraintsToChrome_ = function constraintsToChrome_(c) {
            if ((typeof c === 'undefined' ? 'undefined' : _typeof(c)) !== 'object' || c.mandatory || c.optional) {
              return c;
            }
            let cc = {};
            Object.keys(c).forEach(function (key) {
              if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
                return;
              }
              let r = _typeof(c[key]) === 'object' ? c[key] : {ideal: c[key]};
              if (r.exact !== undefined && typeof r.exact === 'number') {
                r.min = r.max = r.exact;
              }
              let oldname_ = function oldname_(prefix, name) {
                if (prefix) {
                  return prefix + name.charAt(0).toUpperCase() + name.slice(1);
                }
                return name === 'deviceId' ? 'sourceId' : name;
              };
              if (r.ideal !== undefined) {
                cc.optional = cc.optional || [];
                let oc = {};
                if (typeof r.ideal === 'number') {
                  oc[oldname_('min', key)] = r.ideal;
                  cc.optional.push(oc);
                  oc = {};
                  oc[oldname_('max', key)] = r.ideal;
                  cc.optional.push(oc);
                } else {
                  oc[oldname_('', key)] = r.ideal;
                  cc.optional.push(oc);
                }
              }
              if (r.exact !== undefined && typeof r.exact !== 'number') {
                cc.mandatory = cc.mandatory || {};
                cc.mandatory[oldname_('', key)] = r.exact;
              } else {
                ['min', 'max'].forEach(function (mix) {
                  if (r[mix] !== undefined) {
                    cc.mandatory = cc.mandatory || {};
                    cc.mandatory[oldname_(mix, key)] = r[mix];
                  }
                });
              }
            });
            if (c.advanced) {
              cc.optional = (cc.optional || []).concat(c.advanced);
            }
            return cc;
          };

          let shimConstraints_ = function shimConstraints_(constraints, func) {
            if (browserDetails.version >= 61) {
              return func(constraints);
            }
            constraints = JSON.parse(JSON.stringify(constraints));
            if (constraints && _typeof(constraints.audio) === 'object') {
              let remap = function remap(obj, a, b) {
                if (a in obj && !(b in obj)) {
                  obj[b] = obj[a];
                  delete obj[a];
                }
              };
              constraints = JSON.parse(JSON.stringify(constraints));
              remap(constraints.audio, 'autoGainControl', 'googAutoGainControl');
              remap(constraints.audio, 'noiseSuppression', 'googNoiseSuppression');
              constraints.audio = constraintsToChrome_(constraints.audio);
            }
            if (constraints && _typeof(constraints.video) === 'object') {
              // Shim facingMode for mobile & surface pro.
              let face = constraints.video.facingMode;
              face = face && ((typeof face === 'undefined' ? 'undefined' : _typeof(face)) === 'object' ? face : {ideal: face});
              let getSupportedFacingModeLies = browserDetails.version < 66;

              if (face && (face.exact === 'user' || face.exact === 'environment' || face.ideal === 'user' || face.ideal === 'environment') && !(navigator.mediaDevices.getSupportedConstraints && navigator.mediaDevices.getSupportedConstraints().facingMode && !getSupportedFacingModeLies)) {
                delete constraints.video.facingMode;
                let matches = void 0;
                if (face.exact === 'environment' || face.ideal === 'environment') {
                  matches = ['back', 'rear'];
                } else if (face.exact === 'user' || face.ideal === 'user') {
                  matches = ['front'];
                }
                if (matches) {
                  // Look for matches in label, or use last cam for back (typical).
                  return navigator.mediaDevices.enumerateDevices().then(function (devices) {
                    devices = devices.filter(function (d) {
                      return d.kind === 'videoinput';
                    });
                    let dev = devices.find(function (d) {
                      return matches.some(function (match) {
                        return d.label.toLowerCase().includes(match);
                      });
                    });
                    if (!dev && devices.length && matches.includes('back')) {
                      dev = devices[devices.length - 1]; // more likely the back cam
                    }
                    if (dev) {
                      constraints.video.deviceId = face.exact ? {exact: dev.deviceId} : {ideal: dev.deviceId};
                    }
                    constraints.video = constraintsToChrome_(constraints.video);
                    logging('chrome: ' + JSON.stringify(constraints));
                    return func(constraints);
                  });
                }
              }
              constraints.video = constraintsToChrome_(constraints.video);
            }
            logging('chrome: ' + JSON.stringify(constraints));
            return func(constraints);
          };

          let shimError_ = function shimError_(e) {
            if (browserDetails.version >= 64) {
              return e;
            }
            return {
              name: {
                PermissionDeniedError: 'NotAllowedError',
                PermissionDismissedError: 'NotAllowedError',
                InvalidStateError: 'NotAllowedError',
                DevicesNotFoundError: 'NotFoundError',
                ConstraintNotSatisfiedError: 'OverconstrainedError',
                TrackStartError: 'NotReadableError',
                MediaDeviceFailedDueToShutdown: 'NotAllowedError',
                MediaDeviceKillSwitchOn: 'NotAllowedError',
                TabCaptureError: 'AbortError',
                ScreenCaptureError: 'AbortError',
                DeviceCaptureError: 'AbortError'
              }[e.name] || e.name,
              message: e.message,
              constraint: e.constraint || e.constraintName,
              toString: function toString() {
                return this.name + (this.message && ': ') + this.message;
              }
            };
          };

          let getUserMedia_ = function getUserMedia_(constraints, onSuccess, onError) {
            shimConstraints_(constraints, function (c) {
              navigator.webkitGetUserMedia(c, onSuccess, function (e) {
                if (onError) {
                  onError(shimError_(e));
                }
              });
            });
          };
          navigator.getUserMedia = getUserMedia_.bind(navigator);

          // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia
          // function which returns a Promise, it does not accept spec-style
          // constraints.
          if (navigator.mediaDevices.getUserMedia) {
            let origGetUserMedia = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
            navigator.mediaDevices.getUserMedia = function (cs) {
              return shimConstraints_(cs, function (c) {
                return origGetUserMedia(c).then(function (stream) {
                  if (c.audio && !stream.getAudioTracks().length || c.video && !stream.getVideoTracks().length) {
                    stream.getTracks().forEach(function (track) {
                      track.stop();
                    });
                    throw new DOMException('', 'NotFoundError');
                  }
                  return stream;
                }, function (e) {
                  return Promise.reject(shimError_(e));
                });
              });
            };
          }
        }

      }, {"../utils.js": 15}], 6: [function (require, module, exports) {
        /*
 *  Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
        /* eslint-env node */
        'use strict';

        Object.defineProperty(exports, "__esModule", {
          value: true
        });

        let _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
          return typeof obj;
        } : function (obj) {
          return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
        };

        exports.shimRTCIceCandidate = shimRTCIceCandidate;
        exports.shimMaxMessageSize = shimMaxMessageSize;
        exports.shimSendThrowTypeError = shimSendThrowTypeError;
        exports.shimConnectionState = shimConnectionState;
        exports.removeAllowExtmapMixed = removeAllowExtmapMixed;

        let _sdp = require('sdp');

        let _sdp2 = _interopRequireDefault(_sdp);

        let _utils = require('./utils');

        let utils = _interopRequireWildcard(_utils);

        function _interopRequireWildcard(obj) {
          if (obj && obj.__esModule) {
            return obj;
          } else {
            let newObj = {};
            if (obj != null) {
              for (let key in obj) {
                if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key];
              }
            }
            newObj.default = obj;
            return newObj;
          }
        }

        function _interopRequireDefault(obj) {
          return obj && obj.__esModule ? obj : {default: obj};
        }

        function shimRTCIceCandidate(window) {
          // foundation is arbitrarily chosen as an indicator for full support for
          // https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface
          if (!window.RTCIceCandidate || window.RTCIceCandidate && 'foundation' in window.RTCIceCandidate.prototype) {
            return;
          }

          let NativeRTCIceCandidate = window.RTCIceCandidate;
          window.RTCIceCandidate = function RTCIceCandidate(args) {
            // Remove the a= which shouldn't be part of the candidate string.
            if ((typeof args === 'undefined' ? 'undefined' : _typeof(args)) === 'object' && args.candidate && args.candidate.indexOf('a=') === 0) {
              args = JSON.parse(JSON.stringify(args));
              args.candidate = args.candidate.substr(2);
            }

            if (args.candidate && args.candidate.length) {
              // Augment the native candidate with the parsed fields.
              let nativeCandidate = new NativeRTCIceCandidate(args);
              let parsedCandidate = _sdp2.default.parseCandidate(args.candidate);
              let augmentedCandidate = Object.assign(nativeCandidate, parsedCandidate);

              // Add a serializer that does not serialize the extra attributes.
              augmentedCandidate.toJSON = function toJSON() {
                return {
                  candidate: augmentedCandidate.candidate,
                  sdpMid: augmentedCandidate.sdpMid,
                  sdpMLineIndex: augmentedCandidate.sdpMLineIndex,
                  usernameFragment: augmentedCandidate.usernameFragment
                };
              };
              return augmentedCandidate;
            }
            return new NativeRTCIceCandidate(args);
          };
          window.RTCIceCandidate.prototype = NativeRTCIceCandidate.prototype;

          // Hook up the augmented candidate in onicecandidate and
          // addEventListener('icecandidate', ...)
          utils.wrapPeerConnectionEvent(window, 'icecandidate', function (e) {
            if (e.candidate) {
              Object.defineProperty(e, 'candidate', {
                value: new window.RTCIceCandidate(e.candidate),
                writable: 'false'
              });
            }
            return e;
          });
        }

        function shimMaxMessageSize(window) {
          if (!window.RTCPeerConnection) {
            return;
          }
          let browserDetails = utils.detectBrowser(window);

          if (!('sctp' in window.RTCPeerConnection.prototype)) {
            Object.defineProperty(window.RTCPeerConnection.prototype, 'sctp', {
              get: function get() {
                return typeof this._sctp === 'undefined' ? null : this._sctp;
              }
            });
          }

          let sctpInDescription = function sctpInDescription(description) {
            if (!description || !description.sdp) {
              return false;
            }
            let sections = _sdp2.default.splitSections(description.sdp);
            sections.shift();
            return sections.some(function (mediaSection) {
              let mLine = _sdp2.default.parseMLine(mediaSection);
              return mLine && mLine.kind === 'application' && mLine.protocol.indexOf('SCTP') !== -1;
            });
          };

          let getRemoteFirefoxVersion = function getRemoteFirefoxVersion(description) {
            // TO DO: Is there a better solution for detecting Firefox?
            let match = description.sdp.match(/mozilla...THIS_IS_SDPARTA-(\d+)/);
            if (match === null || match.length < 2) {
              return -1;
            }
            let version = parseInt(match[1], 10);
            // Test for NaN (yes, this is ugly)
            return version !== version ? -1 : version;
          };

          let getCanSendMaxMessageSize = function getCanSendMaxMessageSize(remoteIsFirefox) {
            // Every implementation we know can send at least 64 KiB.
            // Note: Although Chrome is technically able to send up to 256 KiB, the
            //       data does not reach the other peer reliably.
            //       See: https://bugs.chromium.org/p/webrtc/issues/detail?id=8419
            let canSendMaxMessageSize = 65536;
            if (browserDetails.browser === 'firefox') {
              if (browserDetails.version < 57) {
                if (remoteIsFirefox === -1) {
                  // FF < 57 will send in 16 KiB chunks using the deprecated PPID
                  // fragmentation.
                  canSendMaxMessageSize = 16384;
                } else {
                  // However, other FF (and RAWRTC) can reassemble PPID-fragmented
                  // messages. Thus, supporting ~2 GiB when sending.
                  canSendMaxMessageSize = 2147483637;
                }
              } else if (browserDetails.version < 60) {
                // Currently, all FF >= 57 will reset the remote maximum message size
                // to the default value when a data channel is created at a later
                // stage. :(
                // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831
                canSendMaxMessageSize = browserDetails.version === 57 ? 65535 : 65536;
              } else {
                // FF >= 60 supports sending ~2 GiB
                canSendMaxMessageSize = 2147483637;
              }
            }
            return canSendMaxMessageSize;
          };

          let getMaxMessageSize = function getMaxMessageSize(description, remoteIsFirefox) {
            // Note: 65536 bytes is the default value from the SDP spec. Also,
            //       every implementation we know supports receiving 65536 bytes.
            let maxMessageSize = 65536;

            // FF 57 has a slightly incorrect default remote max message size, so
            // we need to adjust it here to avoid a failure when sending.
            // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1425697
            if (browserDetails.browser === 'firefox' && browserDetails.version === 57) {
              maxMessageSize = 65535;
            }

            let match = _sdp2.default.matchPrefix(description.sdp, 'a=max-message-size:');
            if (match.length > 0) {
              maxMessageSize = parseInt(match[0].substr(19), 10);
            } else if (browserDetails.browser === 'firefox' && remoteIsFirefox !== -1) {
              // If the maximum message size is not present in the remote SDP and
              // both local and remote are Firefox, the remote peer can receive
              // ~2 GiB.
              maxMessageSize = 2147483637;
            }
            return maxMessageSize;
          };

          let origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription;
          window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription() {
            this._sctp = null;
            // Chrome decided to not expose .sctp in plan-b mode.
            // As usual, adapter.js has to do an 'ugly worakaround'
            // to cover up the mess.
            if (browserDetails.browser === 'chrome' && browserDetails.version >= 76) {
              let _getConfiguration = this.getConfiguration(),
                sdpSemantics = _getConfiguration.sdpSemantics;

              if (sdpSemantics === 'plan-b') {
                Object.defineProperty(this, 'sctp', {
                  get: function get() {
                    return typeof this._sctp === 'undefined' ? null : this._sctp;
                  },

                  enumerable: true,
                  configurable: true
                });
              }
            }

            if (sctpInDescription(arguments[0])) {
              // Check if the remote is FF.
              let isFirefox = getRemoteFirefoxVersion(arguments[0]);

              // Get the maximum message size the local peer is capable of sending
              let canSendMMS = getCanSendMaxMessageSize(isFirefox);

              // Get the maximum message size of the remote peer.
              let remoteMMS = getMaxMessageSize(arguments[0], isFirefox);

              // Determine final maximum message size
              let maxMessageSize = void 0;
              if (canSendMMS === 0 && remoteMMS === 0) {
                maxMessageSize = Number.POSITIVE_INFINITY;
              } else if (canSendMMS === 0 || remoteMMS === 0) {
                maxMessageSize = Math.max(canSendMMS, remoteMMS);
              } else {
                maxMessageSize = Math.min(canSendMMS, remoteMMS);
              }

              // Create a dummy RTCSctpTransport object and the 'maxMessageSize'
              // attribute.
              let sctp = {};
              Object.defineProperty(sctp, 'maxMessageSize', {
                get: function get() {
                  return maxMessageSize;
                }
              });
              this._sctp = sctp;
            }

            return origSetRemoteDescription.apply(this, arguments);
          };
        }

        function shimSendThrowTypeError(window) {
          if (!(window.RTCPeerConnection && 'createDataChannel' in window.RTCPeerConnection.prototype)) {
            return;
          }

          // Note: Although Firefox >= 57 has a native implementation, the maximum
          //       message size can be reset for all data channels at a later stage.
          //       See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831

          function wrapDcSend(dc, pc) {
            let origDataChannelSend = dc.send;
            dc.send = function send() {
              let data = arguments[0];
              let length = data.length || data.size || data.byteLength;
              if (dc.readyState === 'open' && pc.sctp && length > pc.sctp.maxMessageSize) {
                throw new TypeError('Message too large (can send a maximum of ' + pc.sctp.maxMessageSize + ' bytes)');
              }
              return origDataChannelSend.apply(dc, arguments);
            };
          }

          let origCreateDataChannel = window.RTCPeerConnection.prototype.createDataChannel;
          window.RTCPeerConnection.prototype.createDataChannel = function createDataChannel() {
            let dataChannel = origCreateDataChannel.apply(this, arguments);
            wrapDcSend(dataChannel, this);
            return dataChannel;
          };
          utils.wrapPeerConnectionEvent(window, 'datachannel', function (e) {
            wrapDcSend(e.channel, e.target);
            return e;
          });
        }

        /* shims RTCConnectionState by pretending it is the same as iceConnectionState.
 * See https://bugs.chromium.org/p/webrtc/issues/detail?id=6145#c12
 * for why this is a valid hack in Chrome. In Firefox it is slightly incorrect
 * since DTLS failures would be hidden. See
 * https://bugzilla.mozilla.org/show_bug.cgi?id=1265827
 * for the Firefox tracking bug.
 */
        function shimConnectionState(window) {
          if (!window.RTCPeerConnection || 'connectionState' in window.RTCPeerConnection.prototype) {
            return;
          }
          let proto = window.RTCPeerConnection.prototype;
          Object.defineProperty(proto, 'connectionState', {
            get: function get() {
              return {
                completed: 'connected',
                checking: 'connecting'
              }[this.iceConnectionState] || this.iceConnectionState;
            },

            enumerable: true,
            configurable: true
          });
          Object.defineProperty(proto, 'onconnectionstatechange', {
            get: function get() {
              return this._onconnectionstatechange || null;
            },
            set: function set(cb) {
              if (this._onconnectionstatechange) {
                this.removeEventListener('connectionstatechange', this._onconnectionstatechange);
                delete this._onconnectionstatechange;
              }
              if (cb) {
                this.addEventListener('connectionstatechange', this._onconnectionstatechange = cb);
              }
            },

            enumerable: true,
            configurable: true
          });

          ['setLocalDescription', 'setRemoteDescription'].forEach(function (method) {
            let origMethod = proto[method];
            proto[method] = function () {
              if (!this._connectionstatechangepoly) {
                this._connectionstatechangepoly = function (e) {
                  let pc = e.target;
                  if (pc._lastConnectionState !== pc.connectionState) {
                    pc._lastConnectionState = pc.connectionState;
                    let newEvent = new Event('connectionstatechange', e);
                    pc.dispatchEvent(newEvent);
                  }
                  return e;
                };
                this.addEventListener('iceconnectionstatechange', this._connectionstatechangepoly);
              }
              return origMethod.apply(this, arguments);
            };
          });
        }

        function removeAllowExtmapMixed(window) {
          /* remove a=extmap-allow-mixed for webrtc.org < M71 */
          if (!window.RTCPeerConnection) {
            return;
          }
          let browserDetails = utils.detectBrowser(window);
          if (browserDetails.browser === 'chrome' && browserDetails.version >= 71) {
            return;
          }
          if (browserDetails.browser === 'safari' && browserDetails.version >= 605) {
            return;
          }
          let nativeSRD = window.RTCPeerConnection.prototype.setRemoteDescription;
          window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription(desc) {
            if (desc && desc.sdp && desc.sdp.indexOf('\na=extmap-allow-mixed') !== -1) {
              desc.sdp = desc.sdp.split('\n').filter(function (line) {
                return line.trim() !== 'a=extmap-allow-mixed';
              }).join('\n');
            }
            return nativeSRD.apply(this, arguments);
          };
        }

      }, {"./utils": 15, "sdp": 17}], 7: [function (require, module, exports) {
        /*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
        /* eslint-env node */
        'use strict';

        Object.defineProperty(exports, "__esModule", {
          value: true
        });
        exports.shimGetDisplayMedia = exports.shimGetUserMedia = undefined;

        let _getusermedia = require('./getusermedia');

        Object.defineProperty(exports, 'shimGetUserMedia', {
          enumerable: true,
          get: function get() {
            return _getusermedia.shimGetUserMedia;
          }
        });

        let _getdisplaymedia = require('./getdisplaymedia');

        Object.defineProperty(exports, 'shimGetDisplayMedia', {
          enumerable: true,
          get: function get() {
            return _getdisplaymedia.shimGetDisplayMedia;
          }
        });
        exports.shimPeerConnection = shimPeerConnection;
        exports.shimReplaceTrack = shimReplaceTrack;

        let _utils = require('../utils');

        let utils = _interopRequireWildcard(_utils);

        let _filtericeservers = require('./filtericeservers');

        let _rtcpeerconnectionShim = require('rtcpeerconnection-shim');

        let _rtcpeerconnectionShim2 = _interopRequireDefault(_rtcpeerconnectionShim);

        function _interopRequireDefault(obj) {
          return obj && obj.__esModule ? obj : {default: obj};
        }

        function _interopRequireWildcard(obj) {
          if (obj && obj.__esModule) {
            return obj;
          } else {
            let newObj = {};
            if (obj != null) {
              for (let key in obj) {
                if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key];
              }
            }
            newObj.default = obj;
            return newObj;
          }
        }

        function shimPeerConnection(window) {
          let browserDetails = utils.detectBrowser(window);

          if (window.RTCIceGatherer) {
            if (!window.RTCIceCandidate) {
              window.RTCIceCandidate = function RTCIceCandidate(args) {
                return args;
              };
            }
            if (!window.RTCSessionDescription) {
              window.RTCSessionDescription = function RTCSessionDescription(args) {
                return args;
              };
            }
            // this adds an additional event listener to MediaStrackTrack that signals
            // when a tracks enabled property was changed. Workaround for a bug in
            // addStream, see below. No longer required in 15025+
            if (browserDetails.version < 15025) {
              let origMSTEnabled = Object.getOwnPropertyDescriptor(window.MediaStreamTrack.prototype, 'enabled');
              Object.defineProperty(window.MediaStreamTrack.prototype, 'enabled', {
                set: function set(value) {
                  origMSTEnabled.set.call(this, value);
                  let ev = new Event('enabled');
                  ev.enabled = value;
                  this.dispatchEvent(ev);
                }
              });
            }
          }

          // ORTC defines the DTMF sender a bit different.
          // https://github.com/w3c/ortc/issues/714
          if (window.RTCRtpSender && !('dtmf' in window.RTCRtpSender.prototype)) {
            Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', {
              get: function get() {
                if (this._dtmf === undefined) {
                  if (this.track.kind === 'audio') {
                    this._dtmf = new window.RTCDtmfSender(this);
                  } else if (this.track.kind === 'video') {
                    this._dtmf = null;
                  }
                }
                return this._dtmf;
              }
            });
          }
          // Edge currently only implements the RTCDtmfSender, not the
          // RTCDTMFSender alias. See http://draft.ortc.org/#rtcdtmfsender2*
          if (window.RTCDtmfSender && !window.RTCDTMFSender) {
            window.RTCDTMFSender = window.RTCDtmfSender;
          }

          let RTCPeerConnectionShim = (0, _rtcpeerconnectionShim2.default)(window, browserDetails.version);
          window.RTCPeerConnection = config => {
            if (config && config.iceServers) {
              config.iceServers = (0, _filtericeservers.filterIceServers)(config.iceServers, browserDetails.version);
              utils.log('ICE servers after filtering:', config.iceServers);
            }
            return new RTCPeerConnectionShim(config);
          };
          window.RTCPeerConnection.prototype = RTCPeerConnectionShim.prototype;
        }

        function shimReplaceTrack(window) {
          // ORTC has replaceTrack -- https://github.com/w3c/ortc/issues/614
          if (window.RTCRtpSender && !('replaceTrack' in window.RTCRtpSender.prototype)) {
            window.RTCRtpSender.prototype.replaceTrack = window.RTCRtpSender.prototype.setTrack;
          }
        }

      }, {
        "../utils": 15,
        "./filtericeservers": 8,
        "./getdisplaymedia": 9,
        "./getusermedia": 10,
        "rtcpeerconnection-shim": 16
      }], 8: [function (require, module, exports) {
        /*
 *  Copyright (c) 2018 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
        /* eslint-env node */
        'use strict';

        Object.defineProperty(exports, "__esModule", {
          value: true
        });
        exports.filterIceServers = filterIceServers;

        let _utils = require('../utils');

        let utils = _interopRequireWildcard(_utils);

        function _interopRequireWildcard(obj) {
          if (obj && obj.__esModule) {
            return obj;
          } else {
            let newObj = {};
            if (obj != null) {
              for (let key in obj) {
                if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key];
              }
            }
            newObj.default = obj;
            return newObj;
          }
        }

// Edge does not like
// 1) stun: filtered after 14393 unless ?transport=udp is present
// 2) turn: that does not have all of turn:host:port?transport=udp
// 3) turn: with ipv6 addresses
// 4) turn: occurring muliple times
        function filterIceServers(iceServers, edgeVersion) {
          let hasTurn = false;
          iceServers = JSON.parse(JSON.stringify(iceServers));
          return iceServers.filter(function (server) {
            if (server && (server.urls || server.url)) {
              let urls = server.urls || server.url;
              if (server.url && !server.urls) {
                utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls');
              }
              let isString = typeof urls === 'string';
              if (isString) {
                urls = [urls];
              }
              urls = urls.filter(function (url) {
                // filter STUN unconditionally.
                if (url.indexOf('stun:') === 0) {
                  return false;
                }

                let validTurn = url.startsWith('turn') && !url.startsWith('turn:[') && url.includes('transport=udp');
                if (validTurn && !hasTurn) {
                  hasTurn = true;
                  return true;
                }
                return validTurn && !hasTurn;
              });

              delete server.url;
              server.urls = isString ? urls[0] : urls;
              return !!urls.length;
            }
          });
        }

      }, {"../utils": 15}], 9: [function (require, module, exports) {
        /*
 *  Copyright (c) 2018 The adapter.js project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
        /* eslint-env node */
        'use strict';

        Object.defineProperty(exports, "__esModule", {
          value: true
        });
        exports.shimGetDisplayMedia = shimGetDisplayMedia;

        function shimGetDisplayMedia(window) {
          if (!('getDisplayMedia' in window.navigator)) {
            return;
          }
          if (!window.navigator.mediaDevices) {
            return;
          }
          if (window.navigator.mediaDevices && 'getDisplayMedia' in window.navigator.mediaDevices) {
            return;
          }
          window.navigator.mediaDevices.getDisplayMedia = window.navigator.getDisplayMedia.bind(window.navigator);
        }

      }, {}], 10: [function (require, module, exports) {
        /*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
        /* eslint-env node */
        'use strict';

        Object.defineProperty(exports, "__esModule", {
          value: true
        });
        exports.shimGetUserMedia = shimGetUserMedia;

        function shimGetUserMedia(window) {
          let navigator = window && window.navigator;

          let shimError_ = function shimError_(e) {
            return {
              name: {PermissionDeniedError: 'NotAllowedError'}[e.name] || e.name,
              message: e.message,
              constraint: e.constraint,
              toString: function toString() {
                return this.name;
              }
            };
          };

          // getUserMedia error shim.
          let origGetUserMedia = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
          navigator.mediaDevices.getUserMedia = function (c) {
            return origGetUserMedia(c).catch(function (e) {
              return Promise.reject(shimError_(e));
            });
          };
        }

      }, {}], 11: [function (require, module, exports) {
        /*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
        /* eslint-env node */
        'use strict';

        Object.defineProperty(exports, "__esModule", {
          value: true
        });
        exports.shimGetDisplayMedia = exports.shimGetUserMedia = undefined;

        let _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
          return typeof obj;
        } : function (obj) {
          return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
        };

        let _getusermedia = require('./getusermedia');

        Object.defineProperty(exports, 'shimGetUserMedia', {
          enumerable: true,
          get: function get() {
            return _getusermedia.shimGetUserMedia;
          }
        });

        let _getdisplaymedia = require('./getdisplaymedia');

        Object.defineProperty(exports, 'shimGetDisplayMedia', {
          enumerable: true,
          get: function get() {
            return _getdisplaymedia.shimGetDisplayMedia;
          }
        });
        exports.shimOnTrack = shimOnTrack;
        exports.shimPeerConnection = shimPeerConnection;
        exports.shimSenderGetStats = shimSenderGetStats;
        exports.shimReceiverGetStats = shimReceiverGetStats;
        exports.shimRemoveStream = shimRemoveStream;
        exports.shimRTCDataChannel = shimRTCDataChannel;
        exports.shimAddTransceiver = shimAddTransceiver;
        exports.shimGetParameters = shimGetParameters;
        exports.shimCreateOffer = shimCreateOffer;
        exports.shimCreateAnswer = shimCreateAnswer;

        let _utils = require('../utils');

        let utils = _interopRequireWildcard(_utils);

        function _interopRequireWildcard(obj) {
          if (obj && obj.__esModule) {
            return obj;
          } else {
            let newObj = {};
            if (obj != null) {
              for (let key in obj) {
                if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key];
              }
            }
            newObj.default = obj;
            return newObj;
          }
        }

        function _defineProperty(obj, key, value) {
          if (key in obj) {
            Object.defineProperty(obj, key, {value: value, enumerable: true, configurable: true, writable: true});
          } else {
            obj[key] = value;
          }
          return obj;
        }

        function shimOnTrack(window) {
          if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCTrackEvent && 'receiver' in window.RTCTrackEvent.prototype && !('transceiver' in window.RTCTrackEvent.prototype)) {
            Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', {
              get: function get() {
                return {receiver: this.receiver};
              }
            });
          }
        }

        function shimPeerConnection(window) {
          let browserDetails = utils.detectBrowser(window);

          if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) !== 'object' || !(window.RTCPeerConnection || window.mozRTCPeerConnection)) {
            return; // probably media.peerconnection.enabled=false in about:config
          }
          if (!window.RTCPeerConnection && window.mozRTCPeerConnection) {
            // very basic support for old versions.
            window.RTCPeerConnection = window.mozRTCPeerConnection;
          }

          if (browserDetails.version < 53) {
            // shim away need for obsolete RTCIceCandidate/RTCSessionDescription.
            ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'].forEach(function (method) {
              let nativeMethod = window.RTCPeerConnection.prototype[method];
              let methodObj = _defineProperty({}, method, function () {
                arguments[0] = new (method === 'addIceCandidate' ? window.RTCIceCandidate : window.RTCSessionDescription)(arguments[0]);
                return nativeMethod.apply(this, arguments);
              });
              window.RTCPeerConnection.prototype[method] = methodObj[method];
            });
          }

          // support for addIceCandidate(null or undefined)
          // as well as ignoring {sdpMid, candidate: ""}
          if (browserDetails.version < 68) {
            let nativeAddIceCandidate = window.RTCPeerConnection.prototype.addIceCandidate;
            window.RTCPeerConnection.prototype.addIceCandidate = function addIceCandidate() {
              if (!arguments[0]) {
                if (arguments[1]) {
                  arguments[1].apply(null);
                }
                return Promise.resolve();
              }
              // Firefox 68+ emits and processes {candidate: "", ...}, ignore
              // in older versions.
              if (arguments[0] && arguments[0].candidate === '') {
                return Promise.resolve();
              }
              return nativeAddIceCandidate.apply(this, arguments);
            };
          }

          let modernStatsTypes = {
            inboundrtp: 'inbound-rtp',
            outboundrtp: 'outbound-rtp',
            candidatepair: 'candidate-pair',
            localcandidate: 'local-candidate',
            remotecandidate: 'remote-candidate'
          };

          let nativeGetStats = window.RTCPeerConnection.prototype.getStats;
          window.RTCPeerConnection.prototype.getStats = function getStats() {
            let _arguments = Array.prototype.slice.call(arguments),
              selector = _arguments[0],
              onSucc = _arguments[1],
              onErr = _arguments[2];

            return nativeGetStats.apply(this, [selector || null]).then(function (stats) {
              if (browserDetails.version < 53 && !onSucc) {
                // Shim only promise getStats with spec-hyphens in type names
                // Leave callback version alone; misc old uses of forEach before Map
                try {
                  stats.forEach(function (stat) {
                    stat.type = modernStatsTypes[stat.type] || stat.type;
                  });
                } catch (e) {
                  if (e.name !== 'TypeError') {
                    throw e;
                  }
                  // Avoid TypeError: "type" is read-only, in old versions. 34-43ish
                  stats.forEach(function (stat, i) {
                    stats.set(i, Object.assign({}, stat, {
                      type: modernStatsTypes[stat.type] || stat.type
                    }));
                  });
                }
              }
              return stats;
            }).then(onSucc, onErr);
          };
        }

        function shimSenderGetStats(window) {
          if (!((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCPeerConnection && window.RTCRtpSender)) {
            return;
          }
          if (window.RTCRtpSender && 'getStats' in window.RTCRtpSender.prototype) {
            return;
          }
          let origGetSenders = window.RTCPeerConnection.prototype.getSenders;
          if (origGetSenders) {
            window.RTCPeerConnection.prototype.getSenders = function getSenders() {
              let _this = this;

              let senders = origGetSenders.apply(this, []);
              senders.forEach(function (sender) {
                return sender._pc = _this;
              });
              return senders;
            };
          }

          let origAddTrack = window.RTCPeerConnection.prototype.addTrack;
          if (origAddTrack) {
            window.RTCPeerConnection.prototype.addTrack = function addTrack() {
              let sender = origAddTrack.apply(this, arguments);
              sender._pc = this;
              return sender;
            };
          }
          window.RTCRtpSender.prototype.getStats = function getStats() {
            return this.track ? this._pc.getStats(this.track) : Promise.resolve(new Map());
          };
        }

        function shimReceiverGetStats(window) {
          if (!((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCPeerConnection && window.RTCRtpSender)) {
            return;
          }
          if (window.RTCRtpSender && 'getStats' in window.RTCRtpReceiver.prototype) {
            return;
          }
          let origGetReceivers = window.RTCPeerConnection.prototype.getReceivers;
          if (origGetReceivers) {
            window.RTCPeerConnection.prototype.getReceivers = function getReceivers() {
              let _this2 = this;

              let receivers = origGetReceivers.apply(this, []);
              receivers.forEach(function (receiver) {
                return receiver._pc = _this2;
              });
              return receivers;
            };
          }
          utils.wrapPeerConnectionEvent(window, 'track', function (e) {
            e.receiver._pc = e.srcElement;
            return e;
          });
          window.RTCRtpReceiver.prototype.getStats = function getStats() {
            return this._pc.getStats(this.track);
          };
        }

        function shimRemoveStream(window) {
          if (!window.RTCPeerConnection || 'removeStream' in window.RTCPeerConnection.prototype) {
            return;
          }
          window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) {
            let _this3 = this;

            utils.deprecated('removeStream', 'removeTrack');
            this.getSenders().forEach(function (sender) {
              if (sender.track && stream.getTracks().includes(sender.track)) {
                _this3.removeTrack(sender);
              }
            });
          };
        }

        function shimRTCDataChannel(window) {
          // rename DataChannel to RTCDataChannel (native fix in FF60):
          // https://bugzilla.mozilla.org/show_bug.cgi?id=1173851
          if (window.DataChannel && !window.RTCDataChannel) {
            window.RTCDataChannel = window.DataChannel;
          }
        }

        function shimAddTransceiver(window) {
          // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647
          // Firefox ignores the init sendEncodings options passed to addTransceiver
          // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918
          if (!((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCPeerConnection)) {
            return;
          }
          let origAddTransceiver = window.RTCPeerConnection.prototype.addTransceiver;
          if (origAddTransceiver) {
            window.RTCPeerConnection.prototype.addTransceiver = function addTransceiver() {
              this.setParametersPromises = [];
              let initParameters = arguments[1];
              let shouldPerformCheck = initParameters && 'sendEncodings' in initParameters;
              if (shouldPerformCheck) {
                // If sendEncodings params are provided, validate grammar
                initParameters.sendEncodings.forEach(function (encodingParam) {
                  if ('rid' in encodingParam) {
                    let ridRegex = /^[a-z0-9]{0,16}$/i;
                    if (!ridRegex.test(encodingParam.rid)) {
                      throw new TypeError('Invalid RID value provided.');
                    }
                  }
                  if ('scaleResolutionDownBy' in encodingParam) {
                    if (!(parseFloat(encodingParam.scaleResolutionDownBy) >= 1.0)) {
                      throw new RangeError('scale_resolution_down_by must be >= 1.0');
                    }
                  }
                  if ('maxFramerate' in encodingParam) {
                    if (!(parseFloat(encodingParam.maxFramerate) >= 0)) {
                      throw new RangeError('max_framerate must be >= 0.0');
                    }
                  }
                });
              }
              let transceiver = origAddTransceiver.apply(this, arguments);
              if (shouldPerformCheck) {
                // Check if the init options were applied. If not we do this in an
                // asynchronous way and save the promise reference in a global object.
                // This is an ugly hack, but at the same time is way more robust than
                // checking the sender parameters before and after the createOffer
                // Also note that after the createoffer we are not 100% sure that
                // the params were asynchronously applied so we might miss the
                // opportunity to recreate offer.
                let sender = transceiver.sender;

                let params = sender.getParameters();
                if (!('encodings' in params)) {
                  params.encodings = initParameters.sendEncodings;
                  sender.sendEncodings = initParameters.sendEncodings;
                  this.setParametersPromises.push(sender.setParameters(params).then(function () {
                    delete sender.sendEncodings;
                  }).catch(function () {
                    delete sender.sendEncodings;
                  }));
                }
              }
              return transceiver;
            };
          }
        }

        function shimGetParameters(window) {
          if (!((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCRtpSender)) {
            return;
          }
          let origGetParameters = window.RTCRtpSender.prototype.getParameters;
          if (origGetParameters) {
            window.RTCRtpSender.prototype.getParameters = function getParameters() {
              let params = origGetParameters.apply(this, arguments);
              if (!('sendEncodings' in this)) {
                return params;
              }
              return Object.assign({}, {encodings: this.sendEncodings}, params);
            };
          }
        }

        function shimCreateOffer(window) {
          // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647
          // Firefox ignores the init sendEncodings options passed to addTransceiver
          // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918
          if (!((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCPeerConnection)) {
            return;
          }
          let origCreateOffer = window.RTCPeerConnection.prototype.createOffer;
          window.RTCPeerConnection.prototype.createOffer = function createOffer() {
            let _this4 = this,
              _arguments2 = arguments;

            if (this.setParametersPromises && this.setParametersPromises.length) {
              return Promise.all(this.setParametersPromises).then(function () {
                return origCreateOffer.apply(_this4, _arguments2);
              }).finally(function () {
                _this4.setParametersPromises = [];
              });
            }
            return origCreateOffer.apply(this, arguments);
          };
        }

        function shimCreateAnswer(window) {
          // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647
          // Firefox ignores the init sendEncodings options passed to addTransceiver
          // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918
          if (!((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCPeerConnection)) {
            return;
          }
          let origCreateAnswer = window.RTCPeerConnection.prototype.createAnswer;
          window.RTCPeerConnection.prototype.createAnswer = function createAnswer() {
            let _this5 = this,
              _arguments3 = arguments;

            if (this.setParametersPromises && this.setParametersPromises.length) {
              return Promise.all(this.setParametersPromises).then(function () {
                return origCreateAnswer.apply(_this5, _arguments3);
              }).finally(function () {
                _this5.setParametersPromises = [];
              });
            }
            return origCreateAnswer.apply(this, arguments);
          };
        }

      }, {"../utils": 15, "./getdisplaymedia": 12, "./getusermedia": 13}], 12: [function (require, module, exports) {
        /*
 *  Copyright (c) 2018 The adapter.js project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
        /* eslint-env node */
        'use strict';

        Object.defineProperty(exports, "__esModule", {
          value: true
        });
        exports.shimGetDisplayMedia = shimGetDisplayMedia;

        function shimGetDisplayMedia(window, preferredMediaSource) {
          if (window.navigator.mediaDevices && 'getDisplayMedia' in window.navigator.mediaDevices) {
            return;
          }
          if (!window.navigator.mediaDevices) {
            return;
          }
          window.navigator.mediaDevices.getDisplayMedia = function getDisplayMedia(constraints) {
            if (!(constraints && constraints.video)) {
              let err = new DOMException('getDisplayMedia without video ' + 'constraints is undefined');
              err.name = 'NotFoundError';
              // from https://heycam.github.io/webidl/#idl-DOMException-error-names
              err.code = 8;
              return Promise.reject(err);
            }
            if (constraints.video === true) {
              constraints.video = {mediaSource: preferredMediaSource};
            } else {
              constraints.video.mediaSource = preferredMediaSource;
            }
            return window.navigator.mediaDevices.getUserMedia(constraints);
          };
        }

      }, {}], 13: [function (require, module, exports) {
        /*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
        /* eslint-env node */
        'use strict';

        Object.defineProperty(exports, "__esModule", {
          value: true
        });

        let _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
          return typeof obj;
        } : function (obj) {
          return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
        };

        exports.shimGetUserMedia = shimGetUserMedia;

        let _utils = require('../utils');

        let utils = _interopRequireWildcard(_utils);

        function _interopRequireWildcard(obj) {
          if (obj && obj.__esModule) {
            return obj;
          } else {
            let newObj = {};
            if (obj != null) {
              for (let key in obj) {
                if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key];
              }
            }
            newObj.default = obj;
            return newObj;
          }
        }

        function shimGetUserMedia(window) {
          let browserDetails = utils.detectBrowser(window);
          let navigator = window && window.navigator;
          let MediaStreamTrack = window && window.MediaStreamTrack;

          navigator.getUserMedia = function (constraints, onSuccess, onError) {
            // Replace Firefox 44+'s deprecation warning with unprefixed version.
            utils.deprecated('navigator.getUserMedia', 'navigator.mediaDevices.getUserMedia');
            navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError);
          };

          if (!(browserDetails.version > 55 && 'autoGainControl' in navigator.mediaDevices.getSupportedConstraints())) {
            let remap = function remap(obj, a, b) {
              if (a in obj && !(b in obj)) {
                obj[b] = obj[a];
                delete obj[a];
              }
            };

            let nativeGetUserMedia = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
            navigator.mediaDevices.getUserMedia = function (c) {
              if ((typeof c === 'undefined' ? 'undefined' : _typeof(c)) === 'object' && _typeof(c.audio) === 'object') {
                c = JSON.parse(JSON.stringify(c));
                remap(c.audio, 'autoGainControl', 'mozAutoGainControl');
                remap(c.audio, 'noiseSuppression', 'mozNoiseSuppression');
              }
              return nativeGetUserMedia(c);
            };

            if (MediaStreamTrack && MediaStreamTrack.prototype.getSettings) {
              let nativeGetSettings = MediaStreamTrack.prototype.getSettings;
              MediaStreamTrack.prototype.getSettings = function () {
                let obj = nativeGetSettings.apply(this, arguments);
                remap(obj, 'mozAutoGainControl', 'autoGainControl');
                remap(obj, 'mozNoiseSuppression', 'noiseSuppression');
                return obj;
              };
            }

            if (MediaStreamTrack && MediaStreamTrack.prototype.applyConstraints) {
              let nativeApplyConstraints = MediaStreamTrack.prototype.applyConstraints;
              MediaStreamTrack.prototype.applyConstraints = function (c) {
                if (this.kind === 'audio' && (typeof c === 'undefined' ? 'undefined' : _typeof(c)) === 'object') {
                  c = JSON.parse(JSON.stringify(c));
                  remap(c, 'autoGainControl', 'mozAutoGainControl');
                  remap(c, 'noiseSuppression', 'mozNoiseSuppression');
                }
                return nativeApplyConstraints.apply(this, [c]);
              };
            }
          }
        }

      }, {"../utils": 15}], 14: [function (require, module, exports) {
        /*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
        'use strict';

        Object.defineProperty(exports, "__esModule", {
          value: true
        });

        let _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
          return typeof obj;
        } : function (obj) {
          return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
        };

        exports.shimLocalStreamsAPI = shimLocalStreamsAPI;
        exports.shimRemoteStreamsAPI = shimRemoteStreamsAPI;
        exports.shimCallbacksAPI = shimCallbacksAPI;
        exports.shimGetUserMedia = shimGetUserMedia;
        exports.shimConstraints = shimConstraints;
        exports.shimRTCIceServerUrls = shimRTCIceServerUrls;
        exports.shimTrackEventTransceiver = shimTrackEventTransceiver;
        exports.shimCreateOfferLegacy = shimCreateOfferLegacy;
        exports.shimAudioContext = shimAudioContext;

        let _utils = require('../utils');

        let utils = _interopRequireWildcard(_utils);

        function _interopRequireWildcard(obj) {
          if (obj && obj.__esModule) {
            return obj;
          } else {
            let newObj = {};
            if (obj != null) {
              for (let key in obj) {
                if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key];
              }
            }
            newObj.default = obj;
            return newObj;
          }
        }

        function shimLocalStreamsAPI(window) {
          if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) !== 'object' || !window.RTCPeerConnection) {
            return;
          }
          if (!('getLocalStreams' in window.RTCPeerConnection.prototype)) {
            window.RTCPeerConnection.prototype.getLocalStreams = function getLocalStreams() {
              if (!this._localStreams) {
                this._localStreams = [];
              }
              return this._localStreams;
            };
          }
          if (!('addStream' in window.RTCPeerConnection.prototype)) {
            let _addTrack = window.RTCPeerConnection.prototype.addTrack;
            window.RTCPeerConnection.prototype.addStream = function addStream(stream) {
              let _this = this;

              if (!this._localStreams) {
                this._localStreams = [];
              }
              if (!this._localStreams.includes(stream)) {
                this._localStreams.push(stream);
              }
              // Try to emulate Chrome's behaviour of adding in audio-video order.
              // Safari orders by track id.
              stream.getAudioTracks().forEach(function (track) {
                return _addTrack.call(_this, track, stream);
              });
              stream.getVideoTracks().forEach(function (track) {
                return _addTrack.call(_this, track, stream);
              });
            };

            window.RTCPeerConnection.prototype.addTrack = function addTrack(track) {
              let _this2 = this;

              for (let _len = arguments.length, streams = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
                streams[_key - 1] = arguments[_key];
              }

              if (streams) {
                streams.forEach(function (stream) {
                  if (!_this2._localStreams) {
                    _this2._localStreams = [stream];
                  } else if (!_this2._localStreams.includes(stream)) {
                    _this2._localStreams.push(stream);
                  }
                });
              }
              return _addTrack.apply(this, arguments);
            };
          }
          if (!('removeStream' in window.RTCPeerConnection.prototype)) {
            window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) {
              let _this3 = this;

              if (!this._localStreams) {
                this._localStreams = [];
              }
              let index = this._localStreams.indexOf(stream);
              if (index === -1) {
                return;
              }
              this._localStreams.splice(index, 1);
              let tracks = stream.getTracks();
              this.getSenders().forEach(function (sender) {
                if (tracks.includes(sender.track)) {
                  _this3.removeTrack(sender);
                }
              });
            };
          }
        }

        function shimRemoteStreamsAPI(window) {
          if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) !== 'object' || !window.RTCPeerConnection) {
            return;
          }
          if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) {
            window.RTCPeerConnection.prototype.getRemoteStreams = function getRemoteStreams() {
              return this._remoteStreams ? this._remoteStreams : [];
            };
          }
          if (!('onaddstream' in window.RTCPeerConnection.prototype)) {
            Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', {
              get: function get() {
                return this._onaddstream;
              },
              set: function set(f) {
                let _this4 = this;

                if (this._onaddstream) {
                  this.removeEventListener('addstream', this._onaddstream);
                  this.removeEventListener('track', this._onaddstreampoly);
                }
                this.addEventListener('addstream', this._onaddstream = f);
                this.addEventListener('track', this._onaddstreampoly = function (e) {
                  e.streams.forEach(function (stream) {
                    if (!_this4._remoteStreams) {
                      _this4._remoteStreams = [];
                    }
                    if (_this4._remoteStreams.includes(stream)) {
                      return;
                    }
                    _this4._remoteStreams.push(stream);
                    let event = new Event('addstream');
                    event.stream = stream;
                    _this4.dispatchEvent(event);
                  });
                });
              }
            });
            let origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription;
            window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription() {
              let pc = this;
              if (!this._onaddstreampoly) {
                this.addEventListener('track', this._onaddstreampoly = function (e) {
                  e.streams.forEach(function (stream) {
                    if (!pc._remoteStreams) {
                      pc._remoteStreams = [];
                    }
                    if (pc._remoteStreams.indexOf(stream) >= 0) {
                      return;
                    }
                    pc._remoteStreams.push(stream);
                    let event = new Event('addstream');
                    event.stream = stream;
                    pc.dispatchEvent(event);
                  });
                });
              }
              return origSetRemoteDescription.apply(pc, arguments);
            };
          }
        }

        function shimCallbacksAPI(window) {
          if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) !== 'object' || !window.RTCPeerConnection) {
            return;
          }
          let prototype = window.RTCPeerConnection.prototype;
          let origCreateOffer = prototype.createOffer;
          let origCreateAnswer = prototype.createAnswer;
          let setLocalDescription = prototype.setLocalDescription;
          let setRemoteDescription = prototype.setRemoteDescription;
          let addIceCandidate = prototype.addIceCandidate;

          prototype.createOffer = function createOffer(successCallback, failureCallback) {
            let options = arguments.length >= 2 ? arguments[2] : arguments[0];
            let promise = origCreateOffer.apply(this, [options]);
            if (!failureCallback) {
              return promise;
            }
            promise.then(successCallback, failureCallback);
            return Promise.resolve();
          };

          prototype.createAnswer = function createAnswer(successCallback, failureCallback) {
            let options = arguments.length >= 2 ? arguments[2] : arguments[0];
            let promise = origCreateAnswer.apply(this, [options]);
            if (!failureCallback) {
              return promise;
            }
            promise.then(successCallback, failureCallback);
            return Promise.resolve();
          };

          let withCallback = function withCallback(description, successCallback, failureCallback) {
            let promise = setLocalDescription.apply(this, [description]);
            if (!failureCallback) {
              return promise;
            }
            promise.then(successCallback, failureCallback);
            return Promise.resolve();
          };
          prototype.setLocalDescription = withCallback;

          withCallback = function withCallback(description, successCallback, failureCallback) {
            let promise = setRemoteDescription.apply(this, [description]);
            if (!failureCallback) {
              return promise;
            }
            promise.then(successCallback, failureCallback);
            return Promise.resolve();
          };
          prototype.setRemoteDescription = withCallback;

          withCallback = function withCallback(candidate, successCallback, failureCallback) {
            let promise = addIceCandidate.apply(this, [candidate]);
            if (!failureCallback) {
              return promise;
            }
            promise.then(successCallback, failureCallback);
            return Promise.resolve();
          };
          prototype.addIceCandidate = withCallback;
        }

        function shimGetUserMedia(window) {
          let navigator = window && window.navigator;

          if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
            // shim not needed in Safari 12.1
            let mediaDevices = navigator.mediaDevices;
            let _getUserMedia = mediaDevices.getUserMedia.bind(mediaDevices);
            navigator.mediaDevices.getUserMedia = function (constraints) {
              return _getUserMedia(shimConstraints(constraints));
            };
          }

          if (!navigator.getUserMedia && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
            navigator.getUserMedia = function getUserMedia(constraints, cb, errcb) {
              navigator.mediaDevices.getUserMedia(constraints).then(cb, errcb);
            }.bind(navigator);
          }
        }

        function shimConstraints(constraints) {
          if (constraints && constraints.video !== undefined) {
            return Object.assign({}, constraints, {video: utils.compactObject(constraints.video)});
          }

          return constraints;
        }

        function shimRTCIceServerUrls(window) {
          // migrate from non-spec RTCIceServer.url to RTCIceServer.urls
          let OrigPeerConnection = window.RTCPeerConnection;
          window.RTCPeerConnection = function RTCPeerConnection(pcConfig, pcConstraints) {
            if (pcConfig && pcConfig.iceServers) {
              let newIceServers = [];
              for (let i = 0; i < pcConfig.iceServers.length; i++) {
                let server = pcConfig.iceServers[i];
                if (!server.hasOwnProperty('urls') && server.hasOwnProperty('url')) {
                  utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls');
                  server = JSON.parse(JSON.stringify(server));
                  server.urls = server.url;
                  delete server.url;
                  newIceServers.push(server);
                } else {
                  newIceServers.push(pcConfig.iceServers[i]);
                }
              }
              pcConfig.iceServers = newIceServers;
            }
            return new OrigPeerConnection(pcConfig, pcConstraints);
          };
          window.RTCPeerConnection.prototype = OrigPeerConnection.prototype;
          // wrap static methods. Currently just generateCertificate.
          if ('generateCertificate' in window.RTCPeerConnection) {
            Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
              get: function get() {
                return OrigPeerConnection.generateCertificate;
              }
            });
          }
        }

        function shimTrackEventTransceiver(window) {
          // Add event.transceiver member over deprecated event.receiver
          if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCTrackEvent && 'receiver' in window.RTCTrackEvent.prototype && !('transceiver' in window.RTCTrackEvent.prototype)) {
            Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', {
              get: function get() {
                return {receiver: this.receiver};
              }
            });
          }
        }

        function shimCreateOfferLegacy(window) {
          let origCreateOffer = window.RTCPeerConnection.prototype.createOffer;
          window.RTCPeerConnection.prototype.createOffer = function createOffer(offerOptions) {
            if (offerOptions) {
              if (typeof offerOptions.offerToReceiveAudio !== 'undefined') {
                // support bit values
                offerOptions.offerToReceiveAudio = !!offerOptions.offerToReceiveAudio;
              }
              let audioTransceiver = this.getTransceivers().find(function (transceiver) {
                return transceiver.receiver.track.kind === 'audio';
              });
              if (offerOptions.offerToReceiveAudio === false && audioTransceiver) {
                if (audioTransceiver.direction === 'sendrecv') {
                  if (audioTransceiver.setDirection) {
                    audioTransceiver.setDirection('sendonly');
                  } else {
                    audioTransceiver.direction = 'sendonly';
                  }
                } else if (audioTransceiver.direction === 'recvonly') {
                  if (audioTransceiver.setDirection) {
                    audioTransceiver.setDirection('inactive');
                  } else {
                    audioTransceiver.direction = 'inactive';
                  }
                }
              } else if (offerOptions.offerToReceiveAudio === true && !audioTransceiver) {
                this.addTransceiver('audio');
              }

              if (typeof offerOptions.offerToReceiveVideo !== 'undefined') {
                // support bit values
                offerOptions.offerToReceiveVideo = !!offerOptions.offerToReceiveVideo;
              }
              let videoTransceiver = this.getTransceivers().find(function (transceiver) {
                return transceiver.receiver.track.kind === 'video';
              });
              if (offerOptions.offerToReceiveVideo === false && videoTransceiver) {
                if (videoTransceiver.direction === 'sendrecv') {
                  if (videoTransceiver.setDirection) {
                    videoTransceiver.setDirection('sendonly');
                  } else {
                    videoTransceiver.direction = 'sendonly';
                  }
                } else if (videoTransceiver.direction === 'recvonly') {
                  if (videoTransceiver.setDirection) {
                    videoTransceiver.setDirection('inactive');
                  } else {
                    videoTransceiver.direction = 'inactive';
                  }
                }
              } else if (offerOptions.offerToReceiveVideo === true && !videoTransceiver) {
                this.addTransceiver('video');
              }
            }
            return origCreateOffer.apply(this, arguments);
          };
        }

        function shimAudioContext(window) {
          if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) !== 'object' || window.AudioContext) {
            return;
          }
          window.AudioContext = window.webkitAudioContext;
        }

      }, {"../utils": 15}], 15: [function (require, module, exports) {
        /*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
        /* eslint-env node */
        'use strict';

        Object.defineProperty(exports, "__esModule", {
          value: true
        });

        let _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
          return typeof obj;
        } : function (obj) {
          return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
        };

        exports.extractVersion = extractVersion;
        exports.wrapPeerConnectionEvent = wrapPeerConnectionEvent;
        exports.disableLog = disableLog;
        exports.disableWarnings = disableWarnings;
        exports.log = log;
        exports.deprecated = deprecated;
        exports.detectBrowser = detectBrowser;
        exports.compactObject = compactObject;
        exports.walkStats = walkStats;
        exports.filterStats = filterStats;

        function _defineProperty(obj, key, value) {
          if (key in obj) {
            Object.defineProperty(obj, key, {value: value, enumerable: true, configurable: true, writable: true});
          } else {
            obj[key] = value;
          }
          return obj;
        }

        let logDisabled_ = true;
        let deprecationWarnings_ = true;

        /**
         * Extract browser version out of the provided user agent string.
         *
         * @param {!string} uastring userAgent string.
         * //@param {!string} expr Regular expression used as match criteria.
         * @param {object} expr Regular expression used as match criteria.
         * @param {!number} pos position in the version string to be returned.
         * @return {!number} browser version.
         */
        function extractVersion(uastring, expr, pos) {
          let match = uastring.match(expr);
          return match && match.length >= pos && parseInt(match[pos], 10);
        }

// Wraps the peerconnection event eventNameToWrap in a function
// which returns the modified event object (or false to prevent
// the event).
        function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) {
          if (!window.RTCPeerConnection) {
            return;
          }
          let proto = window.RTCPeerConnection.prototype;
          let nativeAddEventListener = proto.addEventListener;
          proto.addEventListener = function (nativeEventName, cb) {
            if (nativeEventName !== eventNameToWrap) {
              return nativeAddEventListener.apply(this, arguments);
            }
            let wrappedCallback = function wrappedCallback(e) {
              let modifiedEvent = wrapper(e);
              if (modifiedEvent) {
                if (cb.handleEvent) {
                  cb.handleEvent(modifiedEvent);
                } else {
                  cb(modifiedEvent);
                }
              }
            };
            this._eventMap = this._eventMap || {};
            if (!this._eventMap[eventNameToWrap]) {
              this._eventMap[eventNameToWrap] = new Map();
            }
            this._eventMap[eventNameToWrap].set(cb, wrappedCallback);
            return nativeAddEventListener.apply(this, [nativeEventName, wrappedCallback]);
          };

          let nativeRemoveEventListener = proto.removeEventListener;
          proto.removeEventListener = function (nativeEventName, cb) {
            if (nativeEventName !== eventNameToWrap || !this._eventMap || !this._eventMap[eventNameToWrap]) {
              return nativeRemoveEventListener.apply(this, arguments);
            }
            if (!this._eventMap[eventNameToWrap].has(cb)) {
              return nativeRemoveEventListener.apply(this, arguments);
            }
            let unwrappedCb = this._eventMap[eventNameToWrap].get(cb);
            this._eventMap[eventNameToWrap].delete(cb);
            if (this._eventMap[eventNameToWrap].size === 0) {
              delete this._eventMap[eventNameToWrap];
            }
            if (Object.keys(this._eventMap).length === 0) {
              delete this._eventMap;
            }
            return nativeRemoveEventListener.apply(this, [nativeEventName, unwrappedCb]);
          };

          Object.defineProperty(proto, 'on' + eventNameToWrap, {
            get: function get() {
              return this['_on' + eventNameToWrap];
            },
            set: function set(cb) {
              if (this['_on' + eventNameToWrap]) {
                this.removeEventListener(eventNameToWrap, this['_on' + eventNameToWrap]);
                delete this['_on' + eventNameToWrap];
              }
              if (cb) {
                this.addEventListener(eventNameToWrap, this['_on' + eventNameToWrap] = cb);
              }
            },

            enumerable: true,
            configurable: true
          });
        }

        function disableLog(bool) {
          if (typeof bool !== 'boolean') {
            return new Error('Argument type: ' + (typeof bool === 'undefined' ? 'undefined' : _typeof(bool)) + '. Please use a boolean.');
          }
          logDisabled_ = bool;
          return bool ? 'adapter.js logging disabled' : 'adapter.js logging enabled';
        }

        /**
         * Disable or enable deprecation warnings
         * @param {!boolean} bool set to true to disable warnings.
         */
        function disableWarnings(bool) {
          if (typeof bool !== 'boolean') {
            return new Error('Argument type: ' + (typeof bool === 'undefined' ? 'undefined' : _typeof(bool)) + '. Please use a boolean.');
          }
          deprecationWarnings_ = !bool;
          return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled');
        }

        function log() {
          if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object') {
            if (logDisabled_) {
              return;
            }
            if (typeof console !== 'undefined' && typeof console.log === 'function') {
              console.log.apply(console, arguments);
            }
          }
        }

        /**
         * Shows a deprecation warning suggesting the modern and spec-compatible API.
         */
        function deprecated(oldMethod, newMethod) {
          if (!deprecationWarnings_) {
            return;
          }
          console.warn(oldMethod + ' is deprecated, please use ' + newMethod + ' instead.');
        }

        /**
         * Browser detector.
         *
         * @return {object} result containing browser and version
         *     properties.
         */
        function detectBrowser(window) {
          let navigator = window.navigator;

          // Returned result object.

          let result = {browser: null, version: null};

          // Fail early if it's not a browser
          if (typeof window === 'undefined' || !window.navigator) {
            result.browser = 'Not a browser.';
            return result;
          }

          if (navigator.mozGetUserMedia) {
            // Firefox.
            result.browser = 'firefox';
            result.version = extractVersion(navigator.userAgent, /Firefox\/(\d+)\./, 1);
          } else if (navigator.webkitGetUserMedia || window.isSecureContext === false && window.webkitRTCPeerConnection && !window.RTCIceGatherer) {
            // Chrome, Chromium, Webview, Opera.
            // Version matches Chrome/WebRTC version.
            // Chrome 74 removed webkitGetUserMedia on http as well so we need the
            // more complicated fallback to webkitRTCPeerConnection.
            result.browser = 'chrome';
            result.version = extractVersion(navigator.userAgent, /Chrom(e|ium)\/(\d+)\./, 2);
          } else if (navigator.mediaDevices && navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) {
            // Edge.
            result.browser = 'edge';
            let edge = /Edge\/(\d+).(\d+)$/;
            result.version = extractVersion(navigator.userAgent, edge, 2);
          } else if (window.RTCPeerConnection && navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) {
            // Safari.
            result.browser = 'safari';
            result.version = extractVersion(navigator.userAgent, /AppleWebKit\/(\d+)\./, 1);
            result.supportsUnifiedPlan = window.RTCRtpTransceiver && 'currentDirection' in window.RTCRtpTransceiver.prototype;
          } else {
            // Default fallthrough: not supported.
            result.browser = 'Not a supported browser.';
            return result;
          }

          return result;
        }

        /**
         * Checks if something is an object.
         *
         * @param {*} val The something you want to check.
         * @return true if val is an object, false otherwise.
         */
        function isObject(val) {
          return Object.prototype.toString.call(val) === '[object Object]';
        }

        /**
         * Remove all empty objects and undefined values
         * from a nested object -- an enhanced and vanilla version
         * of Lodash's `compact`.
         */
        function compactObject(data) {
          if (!isObject(data)) {
            return data;
          }

          return Object.keys(data).reduce(function (accumulator, key) {
            let isObj = isObject(data[key]);
            let value = isObj ? compactObject(data[key]) : data[key];
            let isEmptyObject = isObj && !Object.keys(value).length;
            if (value === undefined || isEmptyObject) {
              return accumulator;
            }
            return Object.assign(accumulator, _defineProperty({}, key, value));
          }, {});
        }

        /* iterates the stats graph recursively. */
        function walkStats(stats, base, resultSet) {
          if (!base || resultSet.has(base.id)) {
            return;
          }
          resultSet.set(base.id, base);
          Object.keys(base).forEach(function (name) {
            if (name.endsWith('Id')) {
              walkStats(stats, stats.get(base[name]), resultSet);
            } else if (name.endsWith('Ids')) {
              base[name].forEach(function (id) {
                walkStats(stats, stats.get(id), resultSet);
              });
            }
          });
        }

        /* filter getStats for a sender/receiver track. */
        function filterStats(result, track, outbound) {
          let streamStatsType = outbound ? 'outbound-rtp' : 'inbound-rtp';
          let filteredResult = new Map();
          if (track === null) {
            return filteredResult;
          }
          let trackStats = [];
          result.forEach(function (value) {
            if (value.type === 'track' && value.trackIdentifier === track.id) {
              trackStats.push(value);
            }
          });
          trackStats.forEach(function (trackStat) {
            result.forEach(function (stats) {
              if (stats.type === streamStatsType && stats.trackId === trackStat.id) {
                walkStats(result, stats, filteredResult);
              }
            });
          });
          return filteredResult;
        }

      }, {}], 16: [function (require, module, exports) {
        /*
 *  Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
        /* eslint-env node */
        'use strict';

        let SDPUtils = require('sdp');

        function fixStatsType(stat) {
          return {
            inboundrtp: 'inbound-rtp',
            outboundrtp: 'outbound-rtp',
            candidatepair: 'candidate-pair',
            localcandidate: 'local-candidate',
            remotecandidate: 'remote-candidate'
          }[stat.type] || stat.type;
        }

        function writeMediaSection(transceiver, caps, type, stream, dtlsRole) {
          let sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);

          // Map ICE parameters (ufrag, pwd) to SDP.
          sdp += SDPUtils.writeIceParameters(
            transceiver.iceGatherer.getLocalParameters());

          // Map DTLS parameters to SDP.
          sdp += SDPUtils.writeDtlsParameters(
            transceiver.dtlsTransport.getLocalParameters(),
            type === 'offer' ? 'actpass' : dtlsRole || 'active');

          sdp += 'a=mid:' + transceiver.mid + '\r\n';

          if (transceiver.rtpSender && transceiver.rtpReceiver) {
            sdp += 'a=sendrecv\r\n';
          } else if (transceiver.rtpSender) {
            sdp += 'a=sendonly\r\n';
          } else if (transceiver.rtpReceiver) {
            sdp += 'a=recvonly\r\n';
          } else {
            sdp += 'a=inactive\r\n';
          }

          if (transceiver.rtpSender) {
            let trackId = transceiver.rtpSender._initialTrackId ||
              transceiver.rtpSender.track.id;
            transceiver.rtpSender._initialTrackId = trackId;
            // spec.
            let msid = 'msid:' + (stream ? stream.id : '-') + ' ' +
              trackId + '\r\n';
            sdp += 'a=' + msid;
            // for Chrome. Legacy should no longer be required.
            sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
              ' ' + msid;

            // RTX
            if (transceiver.sendEncodingParameters[0].rtx) {
              sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
                ' ' + msid;
              sdp += 'a=ssrc-group:FID ' +
                transceiver.sendEncodingParameters[0].ssrc + ' ' +
                transceiver.sendEncodingParameters[0].rtx.ssrc +
                '\r\n';
            }
          }
          // FIX ME: this should be written by writeRtpDescription.
          sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
            ' cname:' + SDPUtils.localCName + '\r\n';
          if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
            sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
              ' cname:' + SDPUtils.localCName + '\r\n';
          }
          return sdp;
        }

// Edge does not like
// 1) stun: filtered after 14393 unless ?transport=udp is present
// 2) turn: that does not have all of turn:host:port?transport=udp
// 3) turn: with ipv6 addresses
// 4) turn: occurring muliple times
        function filterIceServers(iceServers, edgeVersion) {
          let hasTurn = false;
          iceServers = JSON.parse(JSON.stringify(iceServers));
          return iceServers.filter(function (server) {
            if (server && (server.urls || server.url)) {
              let urls = server.urls || server.url;
              if (server.url && !server.urls) {
                console.warn('RTCIceServer.url is deprecated! Use urls instead.');
              }
              let isString = typeof urls === 'string';
              if (isString) {
                urls = [urls];
              }
              urls = urls.filter(function (url) {
                let validTurn = url.indexOf('turn:') === 0 &&
                  url.indexOf('transport=udp') !== -1 &&
                  url.indexOf('turn:[') === -1 &&
                  !hasTurn;

                if (validTurn) {
                  hasTurn = true;
                  return true;
                }
                return url.indexOf('stun:') === 0 && edgeVersion >= 14393 &&
                  url.indexOf('?transport=udp') === -1;
              });

              delete server.url;
              server.urls = isString ? urls[0] : urls;
              return !!urls.length;
            }
          });
        }

// Determines the intersection of local and remote capabilities.
        function getCommonCapabilities(localCapabilities, remoteCapabilities) {
          let commonCapabilities = {
            codecs: [],
            headerExtensions: [],
            fecMechanisms: []
          };

          let findCodecByPayloadType = function (pt, codecs) {
            pt = parseInt(pt, 10);
            for (let i = 0; i < codecs.length; i++) {
              if (codecs[i].payloadType === pt ||
                codecs[i].preferredPayloadType === pt) {
                return codecs[i];
              }
            }
          };

          let rtxCapabilityMatches = function (lRtx, rRtx, lCodecs, rCodecs) {
            let lCodec = findCodecByPayloadType(lRtx.parameters.apt, lCodecs);
            let rCodec = findCodecByPayloadType(rRtx.parameters.apt, rCodecs);
            return lCodec && rCodec &&
              lCodec.name.toLowerCase() === rCodec.name.toLowerCase();
          };

          localCapabilities.codecs.forEach(function (lCodec) {
            for (let i = 0; i < remoteCapabilities.codecs.length; i++) {
              let rCodec = remoteCapabilities.codecs[i];
              if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() &&
                lCodec.clockRate === rCodec.clockRate) {
                if (lCodec.name.toLowerCase() === 'rtx' &&
                  lCodec.parameters && rCodec.parameters.apt) {
                  // for RTX we need to find the local rtx that has a apt
                  // which points to the same local codec as the remote one.
                  if (!rtxCapabilityMatches(lCodec, rCodec,
                    localCapabilities.codecs, remoteCapabilities.codecs)) {
                    continue;
                  }
                }
                rCodec = JSON.parse(JSON.stringify(rCodec)); // deepcopy
                // number of channels is the highest common number of channels
                rCodec.numChannels = Math.min(lCodec.numChannels,
                  rCodec.numChannels);
                // push rCodec so we reply with offerer payload type
                commonCapabilities.codecs.push(rCodec);

                // determine common feedback mechanisms
                rCodec.rtcpFeedback = rCodec.rtcpFeedback.filter(function (fb) {
                  for (let j = 0; j < lCodec.rtcpFeedback.length; j++) {
                    if (lCodec.rtcpFeedback[j].type === fb.type &&
                      lCodec.rtcpFeedback[j].parameter === fb.parameter) {
                      return true;
                    }
                  }
                  return false;
                });
                // FIX ME: also need to determine .parameters
                //  see https://github.com/openpeer/ortc/issues/569
                break;
              }
            }
          });

          localCapabilities.headerExtensions.forEach(function (lHeaderExtension) {
            for (let i = 0; i < remoteCapabilities.headerExtensions.length;
                 i++) {
              let rHeaderExtension = remoteCapabilities.headerExtensions[i];
              if (lHeaderExtension.uri === rHeaderExtension.uri) {
                commonCapabilities.headerExtensions.push(rHeaderExtension);
                break;
              }
            }
          });

          // FIX ME: fecMechanisms
          return commonCapabilities;
        }

// is action=setLocalDescription with type allowed in signalingState
        function isActionAllowedInSignalingState(action, type, signalingState) {
          return {
            offer: {
              setLocalDescription: ['stable', 'have-local-offer'],
              setRemoteDescription: ['stable', 'have-remote-offer']
            },
            answer: {
              setLocalDescription: ['have-remote-offer', 'have-local-pranswer'],
              setRemoteDescription: ['have-local-offer', 'have-remote-pranswer']
            }
          }[type][action].indexOf(signalingState) !== -1;
        }

        function maybeAddCandidate(iceTransport, candidate) {
          // Edge's internal representation adds some fields therefore
          // not all fieldѕ are taken into account.
          let alreadyAdded = iceTransport.getRemoteCandidates()
            .find(function (remoteCandidate) {
              return candidate.foundation === remoteCandidate.foundation &&
                candidate.ip === remoteCandidate.ip &&
                candidate.port === remoteCandidate.port &&
                candidate.priority === remoteCandidate.priority &&
                candidate.protocol === remoteCandidate.protocol &&
                candidate.type === remoteCandidate.type;
            });
          if (!alreadyAdded) {
            iceTransport.addRemoteCandidate(candidate);
          }
          return !alreadyAdded;
        }


        function makeError(name, description) {
          let e = new Error(description);
          e.name = name;
          // legacy error codes from https://heycam.github.io/webidl/#idl-DOMException-error-names
          e.code = {
            NotSupportedError: 9,
            InvalidStateError: 11,
            InvalidAccessError: 15,
            TypeError: undefined,
            OperationError: undefined
          }[name];
          return e;
        }

        module.exports = function (window, edgeVersion) {
          // https://w3c.github.io/mediacapture-main/#mediastream
          // Helper function to add the track to the stream and
          // dispatch the event ourselves.
          function addTrackToStreamAndFireEvent(track, stream) {
            stream.addTrack(track);
            stream.dispatchEvent(new window.MediaStreamTrackEvent('addtrack',
              {track: track}));
          }

          function removeTrackFromStreamAndFireEvent(track, stream) {
            stream.removeTrack(track);
            stream.dispatchEvent(new window.MediaStreamTrackEvent('removetrack',
              {track: track}));
          }

          function fireAddTrack(pc, track, receiver, streams) {
            let trackEvent = new Event('track');
            trackEvent.track = track;
            trackEvent.receiver = receiver;
            trackEvent.transceiver = {receiver: receiver};
            trackEvent.streams = streams;
            window.setTimeout(function () {
              pc._dispatchEvent('track', trackEvent);
            });
          }

          let RTCPeerConnection = function (config) {
            let pc = this;

            let _eventTarget = document.createDocumentFragment();
            ['addEventListener', 'removeEventListener', 'dispatchEvent']
              .forEach(function (method) {
                pc[method] = _eventTarget[method].bind(_eventTarget);
              });

            this.canTrickleIceCandidates = null;

            this.needNegotiation = false;

            this.localStreams = [];
            this.remoteStreams = [];

            this._localDescription = null;
            this._remoteDescription = null;

            this.signalingState = 'stable';
            this.iceConnectionState = 'new';
            this.connectionState = 'new';
            this.iceGatheringState = 'new';

            config = JSON.parse(JSON.stringify(config || {}));

            this.usingBundle = config.bundlePolicy === 'max-bundle';
            if (config.rtcpMuxPolicy === 'negotiate') {
              throw(makeError('NotSupportedError',
                'rtcpMuxPolicy \'negotiate\' is not supported'));
            } else if (!config.rtcpMuxPolicy) {
              config.rtcpMuxPolicy = 'require';
            }

            switch (config.iceTransportPolicy) {
              case 'all':
              case 'relay':
                break;
              default:
                config.iceTransportPolicy = 'all';
                break;
            }

            switch (config.bundlePolicy) {
              case 'balanced':
              case 'max-compat':
              case 'max-bundle':
                break;
              default:
                config.bundlePolicy = 'balanced';
                break;
            }

            config.iceServers = filterIceServers(config.iceServers || [], edgeVersion);

            this._iceGatherers = [];
            if (config.iceCandidatePoolSize) {
              for (let i = config.iceCandidatePoolSize; i > 0; i--) {
                this._iceGatherers.push(new window.RTCIceGatherer({
                  iceServers: config.iceServers,
                  gatherPolicy: config.iceTransportPolicy
                }));
              }
            } else {
              config.iceCandidatePoolSize = 0;
            }

            this._config = config;

            // per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ...
            // everything that is needed to describe a SDP m-line.
            this.transceivers = [];

            this._sdpSessionId = SDPUtils.generateSessionId();
            this._sdpSessionVersion = 0;

            this._dtlsRole = undefined; // role for a=setup to use in answers.

            this._isClosed = false;
          };

          Object.defineProperty(RTCPeerConnection.prototype, 'localDescription', {
            configurable: true,
            get: function () {
              return this._localDescription;
            }
          });
          Object.defineProperty(RTCPeerConnection.prototype, 'remoteDescription', {
            configurable: true,
            get: function () {
              return this._remoteDescription;
            }
          });

          // set up event handlers on prototype
          RTCPeerConnection.prototype.onicecandidate = null;
          RTCPeerConnection.prototype.onaddstream = null;
          RTCPeerConnection.prototype.ontrack = null;
          RTCPeerConnection.prototype.onremovestream = null;
          RTCPeerConnection.prototype.onsignalingstatechange = null;
          RTCPeerConnection.prototype.oniceconnectionstatechange = null;
          RTCPeerConnection.prototype.onconnectionstatechange = null;
          RTCPeerConnection.prototype.onicegatheringstatechange = null;
          RTCPeerConnection.prototype.onnegotiationneeded = null;
          RTCPeerConnection.prototype.ondatachannel = null;

          RTCPeerConnection.prototype._dispatchEvent = function (name, event) {
            if (this._isClosed) {
              return;
            }
            this.dispatchEvent(event);
            if (typeof this['on' + name] === 'function') {
              this['on' + name](event);
            }
          };

          RTCPeerConnection.prototype._emitGatheringStateChange = function () {
            let event = new Event('icegatheringstatechange');
            this._dispatchEvent('icegatheringstatechange', event);
          };

          RTCPeerConnection.prototype.getConfiguration = function () {
            return this._config;
          };

          RTCPeerConnection.prototype.getLocalStreams = function () {
            return this.localStreams;
          };

          RTCPeerConnection.prototype.getRemoteStreams = function () {
            return this.remoteStreams;
          };

          // internal helper to create a transceiver object.
          // (which is not yet the same as the WebRTC 1.0 transceiver)
          RTCPeerConnection.prototype._createTransceiver = function (kind, doNotAdd) {
            let hasBundleTransport = this.transceivers.length > 0;
            let transceiver = {
              track: null,
              iceGatherer: null,
              iceTransport: null,
              dtlsTransport: null,
              localCapabilities: null,
              remoteCapabilities: null,
              rtpSender: null,
              rtpReceiver: null,
              kind: kind,
              mid: null,
              sendEncodingParameters: null,
              recvEncodingParameters: null,
              stream: null,
              associatedRemoteMediaStreams: [],
              wantReceive: true
            };
            if (this.usingBundle && hasBundleTransport) {
              transceiver.iceTransport = this.transceivers[0].iceTransport;
              transceiver.dtlsTransport = this.transceivers[0].dtlsTransport;
            } else {
              let transports = this._createIceAndDtlsTransports();
              transceiver.iceTransport = transports.iceTransport;
              transceiver.dtlsTransport = transports.dtlsTransport;
            }
            if (!doNotAdd) {
              this.transceivers.push(transceiver);
            }
            return transceiver;
          };

          RTCPeerConnection.prototype.addTrack = function (track, stream) {
            if (this._isClosed) {
              throw makeError('InvalidStateError',
                'Attempted to call addTrack on a closed peerconnection.');
            }

            let alreadyExists = this.transceivers.find(function (s) {
              return s.track === track;
            });

            if (alreadyExists) {
              throw makeError('InvalidAccessError', 'Track already exists.');
            }

            let transceiver;
            for (let i = 0; i < this.transceivers.length; i++) {
              if (!this.transceivers[i].track &&
                this.transceivers[i].kind === track.kind) {
                transceiver = this.transceivers[i];
              }
            }
            if (!transceiver) {
              transceiver = this._createTransceiver(track.kind);
            }

            this._maybeFireNegotiationNeeded();

            if (this.localStreams.indexOf(stream) === -1) {
              this.localStreams.push(stream);
            }

            transceiver.track = track;
            transceiver.stream = stream;
            transceiver.rtpSender = new window.RTCRtpSender(track,
              transceiver.dtlsTransport);
            return transceiver.rtpSender;
          };

          RTCPeerConnection.prototype.addStream = function (stream) {
            let pc = this;
            if (edgeVersion >= 15025) {
              stream.getTracks().forEach(function (track) {
                pc.addTrack(track, stream);
              });
            } else {
              // Clone is necessary for local demos mostly, attaching directly
              // to two different senders does not work (build 10547).
              // Fixed in 15025 (or earlier)
              let clonedStream = stream.clone();
              stream.getTracks().forEach(function (track, idx) {
                let clonedTrack = clonedStream.getTracks()[idx];
                track.addEventListener('enabled', function (event) {
                  clonedTrack.enabled = event.enabled;
                });
              });
              clonedStream.getTracks().forEach(function (track) {
                pc.addTrack(track, clonedStream);
              });
            }
          };

          RTCPeerConnection.prototype.removeTrack = function (sender) {
            if (this._isClosed) {
              throw makeError('InvalidStateError',
                'Attempted to call removeTrack on a closed peerconnection.');
            }

            if (!(sender instanceof window.RTCRtpSender)) {
              throw new TypeError('Argument 1 of RTCPeerConnection.removeTrack ' +
                'does not implement interface RTCRtpSender.');
            }

            let transceiver = this.transceivers.find(function (t) {
              return t.rtpSender === sender;
            });

            if (!transceiver) {
              throw makeError('InvalidAccessError',
                'Sender was not created by this connection.');
            }
            let stream = transceiver.stream;

            transceiver.rtpSender.stop();
            transceiver.rtpSender = null;
            transceiver.track = null;
            transceiver.stream = null;

            // remove the stream from the set of local streams
            let localStreams = this.transceivers.map(function (t) {
              return t.stream;
            });
            if (localStreams.indexOf(stream) === -1 &&
              this.localStreams.indexOf(stream) > -1) {
              this.localStreams.splice(this.localStreams.indexOf(stream), 1);
            }

            this._maybeFireNegotiationNeeded();
          };

          RTCPeerConnection.prototype.removeStream = function (stream) {
            let pc = this;
            stream.getTracks().forEach(function (track) {
              let sender = pc.getSenders().find(function (s) {
                return s.track === track;
              });
              if (sender) {
                pc.removeTrack(sender);
              }
            });
          };

          RTCPeerConnection.prototype.getSenders = function () {
            return this.transceivers.filter(function (transceiver) {
              return !!transceiver.rtpSender;
            })
              .map(function (transceiver) {
                return transceiver.rtpSender;
              });
          };

          RTCPeerConnection.prototype.getReceivers = function () {
            return this.transceivers.filter(function (transceiver) {
              return !!transceiver.rtpReceiver;
            })
              .map(function (transceiver) {
                return transceiver.rtpReceiver;
              });
          };


          RTCPeerConnection.prototype._createIceGatherer = function (sdpMLineIndex,
                                                                     usingBundle) {
            let pc = this;
            if (usingBundle && sdpMLineIndex > 0) {
              return this.transceivers[0].iceGatherer;
            } else if (this._iceGatherers.length) {
              return this._iceGatherers.shift();
            }
            let iceGatherer = new window.RTCIceGatherer({
              iceServers: this._config.iceServers,
              gatherPolicy: this._config.iceTransportPolicy
            });
            Object.defineProperty(iceGatherer, 'state',
              {value: 'new', writable: true}
            );

            this.transceivers[sdpMLineIndex].bufferedCandidateEvents = [];
            this.transceivers[sdpMLineIndex].bufferCandidates = function (event) {
              let end = !event.candidate || Object.keys(event.candidate).length === 0;
              // polyfill since RTCIceGatherer.state is not implemented in
              // Edge 10547 yet.
              iceGatherer.state = end ? 'completed' : 'gathering';
              if (pc.transceivers[sdpMLineIndex].bufferedCandidateEvents !== null) {
                pc.transceivers[sdpMLineIndex].bufferedCandidateEvents.push(event);
              }
            };
            iceGatherer.addEventListener('localcandidate',
              this.transceivers[sdpMLineIndex].bufferCandidates);
            return iceGatherer;
          };

          // start gathering from an RTCIceGatherer.
          RTCPeerConnection.prototype._gather = function (mid, sdpMLineIndex) {
            let pc = this;
            let iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer;
            if (iceGatherer.onlocalcandidate) {
              return;
            }
            let bufferedCandidateEvents =
              this.transceivers[sdpMLineIndex].bufferedCandidateEvents;
            this.transceivers[sdpMLineIndex].bufferedCandidateEvents = null;
            iceGatherer.removeEventListener('localcandidate',
              this.transceivers[sdpMLineIndex].bufferCandidates);
            iceGatherer.onlocalcandidate = function (evt) {
              if (pc.usingBundle && sdpMLineIndex > 0) {
                // if we know that we use bundle we can drop candidates with
                // ѕdpMLineIndex > 0. If we don't do this then our state gets
                // confused since we dispose the extra ice gatherer.
                return;
              }
              let event = new Event('icecandidate');
              event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex};

              let cand = evt.candidate;
              // Edge emits an empty object for RTCIceCandidateComplete‥
              let end = !cand || Object.keys(cand).length === 0;
              if (end) {
                // polyfill since RTCIceGatherer.state is not implemented in
                // Edge 10547 yet.
                if (iceGatherer.state === 'new' || iceGatherer.state === 'gathering') {
                  iceGatherer.state = 'completed';
                }
              } else {
                if (iceGatherer.state === 'new') {
                  iceGatherer.state = 'gathering';
                }
                // RTCIceCandidate doesn't have a component, needs to be added
                cand.component = 1;
                // also the usernameFragment. TO DO: update SDP to take both variants.
                cand.ufrag = iceGatherer.getLocalParameters().usernameFragment;

                let serializedCandidate = SDPUtils.writeCandidate(cand);
                event.candidate = Object.assign(event.candidate,
                  SDPUtils.parseCandidate(serializedCandidate));

                event.candidate.candidate = serializedCandidate;
                event.candidate.toJSON = function () {
                  return {
                    candidate: event.candidate.candidate,
                    sdpMid: event.candidate.sdpMid,
                    sdpMLineIndex: event.candidate.sdpMLineIndex,
                    usernameFragment: event.candidate.usernameFragment
                  };
                };
              }

              // update local description.
              let sections = SDPUtils.getMediaSections(pc._localDescription.sdp);
              if (!end) {
                sections[event.candidate.sdpMLineIndex] +=
                  'a=' + event.candidate.candidate + '\r\n';
              } else {
                sections[event.candidate.sdpMLineIndex] +=
                  'a=end-of-candidates\r\n';
              }
              pc._localDescription.sdp =
                SDPUtils.getDescription(pc._localDescription.sdp) +
                sections.join('');
              let complete = pc.transceivers.every(function (transceiver) {
                return transceiver.iceGatherer &&
                  transceiver.iceGatherer.state === 'completed';
              });

              if (pc.iceGatheringState !== 'gathering') {
                pc.iceGatheringState = 'gathering';
                pc._emitGatheringStateChange();
              }

              // Emit candidate. Also emit null candidate when all gatherers are
              // complete.
              if (!end) {
                pc._dispatchEvent('icecandidate', event);
              }
              if (complete) {
                pc._dispatchEvent('icecandidate', new Event('icecandidate'));
                pc.iceGatheringState = 'complete';
                pc._emitGatheringStateChange();
              }
            };

            // emit already gathered candidates.
            window.setTimeout(function () {
              bufferedCandidateEvents.forEach(function (e) {
                iceGatherer.onlocalcandidate(e);
              });
            }, 0);
          };

          // Create ICE transport and DTLS transport.
          RTCPeerConnection.prototype._createIceAndDtlsTransports = function () {
            let pc = this;
            let iceTransport = new window.RTCIceTransport(null);
            iceTransport.onicestatechange = function () {
              pc._updateIceConnectionState();
              pc._updateConnectionState();
            };

            let dtlsTransport = new window.RTCDtlsTransport(iceTransport);
            dtlsTransport.ondtlsstatechange = function () {
              pc._updateConnectionState();
            };
            dtlsTransport.onerror = function () {
              // onerror does not set state to failed by itself.
              Object.defineProperty(dtlsTransport, 'state',
                {value: 'failed', writable: true});
              pc._updateConnectionState();
            };

            return {
              iceTransport: iceTransport,
              dtlsTransport: dtlsTransport
            };
          };

          // Destroy ICE gatherer, ICE transport and DTLS transport.
          // Without triggering the callbacks.
          RTCPeerConnection.prototype._disposeIceAndDtlsTransports = function (
            sdpMLineIndex) {
            let iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer;
            if (iceGatherer) {
              delete iceGatherer.onlocalcandidate;
              delete this.transceivers[sdpMLineIndex].iceGatherer;
            }
            let iceTransport = this.transceivers[sdpMLineIndex].iceTransport;
            if (iceTransport) {
              delete iceTransport.onicestatechange;
              delete this.transceivers[sdpMLineIndex].iceTransport;
            }
            let dtlsTransport = this.transceivers[sdpMLineIndex].dtlsTransport;
            if (dtlsTransport) {
              delete dtlsTransport.ondtlsstatechange;
              delete dtlsTransport.onerror;
              delete this.transceivers[sdpMLineIndex].dtlsTransport;
            }
          };

          // Start the RTP Sender and Receiver for a transceiver.
          RTCPeerConnection.prototype._transceive = function (transceiver,
                                                              send, recv) {
            let params = getCommonCapabilities(transceiver.localCapabilities,
              transceiver.remoteCapabilities);
            if (send && transceiver.rtpSender) {
              params.encodings = transceiver.sendEncodingParameters;
              params.rtcp = {
                cname: SDPUtils.localCName,
                compound: transceiver.rtcpParameters.compound
              };
              if (transceiver.recvEncodingParameters.length) {
                params.rtcp.ssrc = transceiver.recvEncodingParameters[0].ssrc;
              }
              transceiver.rtpSender.send(params);
            }
            if (recv && transceiver.rtpReceiver && params.codecs.length > 0) {
              // remove RTX field in Edge 14942
              if (transceiver.kind === 'video'
                && transceiver.recvEncodingParameters
                && edgeVersion < 15019) {
                transceiver.recvEncodingParameters.forEach(function (p) {
                  delete p.rtx;
                });
              }
              if (transceiver.recvEncodingParameters.length) {
                params.encodings = transceiver.recvEncodingParameters;
              } else {
                params.encodings = [{}];
              }
              params.rtcp = {
                compound: transceiver.rtcpParameters.compound
              };
              if (transceiver.rtcpParameters.cname) {
                params.rtcp.cname = transceiver.rtcpParameters.cname;
              }
              if (transceiver.sendEncodingParameters.length) {
                params.rtcp.ssrc = transceiver.sendEncodingParameters[0].ssrc;
              }
              transceiver.rtpReceiver.receive(params);
            }
          };

          RTCPeerConnection.prototype.setLocalDescription = function (description) {
            let pc = this;

            // Note: pranswer is not supported.
            if (['offer', 'answer'].indexOf(description.type) === -1) {
              return Promise.reject(makeError('TypeError',
                'Unsupported type "' + description.type + '"'));
            }

            if (!isActionAllowedInSignalingState('setLocalDescription',
              description.type, pc.signalingState) || pc._isClosed) {
              return Promise.reject(makeError('InvalidStateError',
                'Can not set local ' + description.type +
                ' in state ' + pc.signalingState));
            }

            let sections;
            let sessionpart;
            if (description.type === 'offer') {
              // VERY limited support for SDP munging. Limited to:
              // * changing the order of codecs
              sections = SDPUtils.splitSections(description.sdp);
              sessionpart = sections.shift();
              sections.forEach(function (mediaSection, sdpMLineIndex) {
                let caps = SDPUtils.parseRtpParameters(mediaSection);
                pc.transceivers[sdpMLineIndex].localCapabilities = caps;
              });

              pc.transceivers.forEach(function (transceiver, sdpMLineIndex) {
                pc._gather(transceiver.mid, sdpMLineIndex);
              });
            } else if (description.type === 'answer') {
              sections = SDPUtils.splitSections(pc._remoteDescription.sdp);
              sessionpart = sections.shift();
              let isIceLite = SDPUtils.matchPrefix(sessionpart,
                'a=ice-lite').length > 0;
              sections.forEach(function (mediaSection, sdpMLineIndex) {
                let transceiver = pc.transceivers[sdpMLineIndex];
                let iceGatherer = transceiver.iceGatherer;
                let iceTransport = transceiver.iceTransport;
                let dtlsTransport = transceiver.dtlsTransport;
                let localCapabilities = transceiver.localCapabilities;
                let remoteCapabilities = transceiver.remoteCapabilities;

                // treat bundle-only as not-rejected.
                let rejected = SDPUtils.isRejected(mediaSection) &&
                  SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 0;

                if (!rejected && !transceiver.rejected) {
                  let remoteIceParameters = SDPUtils.getIceParameters(
                    mediaSection, sessionpart);
                  let remoteDtlsParameters = SDPUtils.getDtlsParameters(
                    mediaSection, sessionpart);
                  if (isIceLite) {
                    remoteDtlsParameters.role = 'server';
                  }

                  if (!pc.usingBundle || sdpMLineIndex === 0) {
                    pc._gather(transceiver.mid, sdpMLineIndex);
                    if (iceTransport.state === 'new') {
                      iceTransport.start(iceGatherer, remoteIceParameters,
                        isIceLite ? 'controlling' : 'controlled');
                    }
                    if (dtlsTransport.state === 'new') {
                      dtlsTransport.start(remoteDtlsParameters);
                    }
                  }

                  // Calculate intersection of capabilities.
                  let params = getCommonCapabilities(localCapabilities,
                    remoteCapabilities);

                  // Start the RTCRtpSender. The RTCRtpReceiver for this
                  // transceiver has already been started in setRemoteDescription.
                  pc._transceive(transceiver,
                    params.codecs.length > 0,
                    false);
                }
              });
            }

            pc._localDescription = {
              type: description.type,
              sdp: description.sdp
            };
            if (description.type === 'offer') {
              pc._updateSignalingState('have-local-offer');
            } else {
              pc._updateSignalingState('stable');
            }

            return Promise.resolve();
          };

          RTCPeerConnection.prototype.setRemoteDescription = function (description) {
            let pc = this;

            // Note: pranswer is not supported.
            if (['offer', 'answer'].indexOf(description.type) === -1) {
              return Promise.reject(makeError('TypeError',
                'Unsupported type "' + description.type + '"'));
            }

            if (!isActionAllowedInSignalingState('setRemoteDescription',
              description.type, pc.signalingState) || pc._isClosed) {
              return Promise.reject(makeError('InvalidStateError',
                'Can not set remote ' + description.type +
                ' in state ' + pc.signalingState));
            }

            let streams = {};
            pc.remoteStreams.forEach(function (stream) {
              streams[stream.id] = stream;
            });
            let receiverList = [];
            let sections = SDPUtils.splitSections(description.sdp);
            let sessionpart = sections.shift();
            let isIceLite = SDPUtils.matchPrefix(sessionpart,
              'a=ice-lite').length > 0;
            let usingBundle = SDPUtils.matchPrefix(sessionpart,
              'a=group:BUNDLE ').length > 0;
            pc.usingBundle = usingBundle;
            let iceOptions = SDPUtils.matchPrefix(sessionpart,
              'a=ice-options:')[0];
            if (iceOptions) {
              pc.canTrickleIceCandidates = iceOptions.substr(14).split(' ')
                .indexOf('trickle') >= 0;
            } else {
              pc.canTrickleIceCandidates = false;
            }

            sections.forEach(function (mediaSection, sdpMLineIndex) {
              let lines = SDPUtils.splitLines(mediaSection);
              let kind = SDPUtils.getKind(mediaSection);
              // treat bundle-only as not-rejected.
              let rejected = SDPUtils.isRejected(mediaSection) &&
                SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 0;
              let protocol = lines[0].substr(2).split(' ')[2];

              let direction = SDPUtils.getDirection(mediaSection, sessionpart);
              let remoteMsid = SDPUtils.parseMsid(mediaSection);

              let mid = SDPUtils.getMid(mediaSection) || SDPUtils.generateIdentifier();

              // Reject datachannels which are not implemented yet.
              if (rejected || (kind === 'application' && (protocol === 'DTLS/SCTP' ||
                protocol === 'UDP/DTLS/SCTP'))) {
                // TO DO: this is dangerous in the case where a non-rejected m-line
                //     becomes rejected.
                pc.transceivers[sdpMLineIndex] = {
                  mid: mid,
                  kind: kind,
                  protocol: protocol,
                  rejected: true
                };
                return;
              }

              if (!rejected && pc.transceivers[sdpMLineIndex] &&
                pc.transceivers[sdpMLineIndex].rejected) {
                // recycle a rejected transceiver.
                pc.transceivers[sdpMLineIndex] = pc._createTransceiver(kind, true);
              }

              let transceiver;
              let iceGatherer;
              let iceTransport;
              let dtlsTransport;
              let rtpReceiver;
              let sendEncodingParameters;
              let recvEncodingParameters;
              let localCapabilities;

              let track;
              // FIX ME: ensure the mediaSection has rtcp-mux set.
              let remoteCapabilities = SDPUtils.parseRtpParameters(mediaSection);
              let remoteIceParameters;
              let remoteDtlsParameters;
              if (!rejected) {
                remoteIceParameters = SDPUtils.getIceParameters(mediaSection,
                  sessionpart);
                remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection,
                  sessionpart);
                remoteDtlsParameters.role = 'client';
              }
              recvEncodingParameters =
                SDPUtils.parseRtpEncodingParameters(mediaSection);

              let rtcpParameters = SDPUtils.parseRtcpParameters(mediaSection);

              let isComplete = SDPUtils.matchPrefix(mediaSection,
                'a=end-of-candidates', sessionpart).length > 0;
              let cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:')
                .map(function (cand) {
                  return SDPUtils.parseCandidate(cand);
                })
                .filter(function (cand) {
                  return cand.component === 1;
                });

              // Check if we can use BUNDLE and dispose transports.
              if ((description.type === 'offer' || description.type === 'answer') &&
                !rejected && usingBundle && sdpMLineIndex > 0 &&
                pc.transceivers[sdpMLineIndex]) {
                pc._disposeIceAndDtlsTransports(sdpMLineIndex);
                pc.transceivers[sdpMLineIndex].iceGatherer =
                  pc.transceivers[0].iceGatherer;
                pc.transceivers[sdpMLineIndex].iceTransport =
                  pc.transceivers[0].iceTransport;
                pc.transceivers[sdpMLineIndex].dtlsTransport =
                  pc.transceivers[0].dtlsTransport;
                if (pc.transceivers[sdpMLineIndex].rtpSender) {
                  pc.transceivers[sdpMLineIndex].rtpSender.setTransport(
                    pc.transceivers[0].dtlsTransport);
                }
                if (pc.transceivers[sdpMLineIndex].rtpReceiver) {
                  pc.transceivers[sdpMLineIndex].rtpReceiver.setTransport(
                    pc.transceivers[0].dtlsTransport);
                }
              }
              if (description.type === 'offer' && !rejected) {
                transceiver = pc.transceivers[sdpMLineIndex] ||
                  pc._createTransceiver(kind);
                transceiver.mid = mid;

                if (!transceiver.iceGatherer) {
                  transceiver.iceGatherer = pc._createIceGatherer(sdpMLineIndex,
                    usingBundle);
                }

                if (cands.length && transceiver.iceTransport.state === 'new') {
                  if (isComplete && (!usingBundle || sdpMLineIndex === 0)) {
                    transceiver.iceTransport.setRemoteCandidates(cands);
                  } else {
                    cands.forEach(function (candidate) {
                      maybeAddCandidate(transceiver.iceTransport, candidate);
                    });
                  }
                }

                localCapabilities = window.RTCRtpReceiver.getCapabilities(kind);

                // filter RTX until additional stuff needed for RTX is implemented
                // in adapter.js
                if (edgeVersion < 15019) {
                  localCapabilities.codecs = localCapabilities.codecs.filter(
                    function (codec) {
                      return codec.name !== 'rtx';
                    });
                }

                sendEncodingParameters = transceiver.sendEncodingParameters || [{
                  ssrc: (2 * sdpMLineIndex + 2) * 1001
                }];

                // TO DO: rewrite to use http://w3c.github.io/webrtc-pc/#set-associated-remote-streams
                let isNewTrack = false;
                if (direction === 'sendrecv' || direction === 'sendonly') {
                  isNewTrack = !transceiver.rtpReceiver;
                  rtpReceiver = transceiver.rtpReceiver ||
                    new window.RTCRtpReceiver(transceiver.dtlsTransport, kind);

                  if (isNewTrack) {
                    let stream;
                    track = rtpReceiver.track;
                    // FIX ME: does not work with Plan B.
                    if (remoteMsid && remoteMsid.stream === '-') {
                      // no-op. a stream id of '-' means: no associated stream.
                    } else if (remoteMsid) {
                      if (!streams[remoteMsid.stream]) {
                        streams[remoteMsid.stream] = new window.MediaStream();
                        Object.defineProperty(streams[remoteMsid.stream], 'id', {
                          get: function () {
                            return remoteMsid.stream;
                          }
                        });
                      }
                      Object.defineProperty(track, 'id', {
                        get: function () {
                          return remoteMsid.track;
                        }
                      });
                      stream = streams[remoteMsid.stream];
                    } else {
                      if (!streams.default) {
                        streams.default = new window.MediaStream();
                      }
                      stream = streams.default;
                    }
                    if (stream) {
                      addTrackToStreamAndFireEvent(track, stream);
                      transceiver.associatedRemoteMediaStreams.push(stream);
                    }
                    receiverList.push([track, rtpReceiver, stream]);
                  }
                } else if (transceiver.rtpReceiver && transceiver.rtpReceiver.track) {
                  transceiver.associatedRemoteMediaStreams.forEach(function (s) {
                    let nativeTrack = s.getTracks().find(function (t) {
                      return t.id === transceiver.rtpReceiver.track.id;
                    });
                    if (nativeTrack) {
                      removeTrackFromStreamAndFireEvent(nativeTrack, s);
                    }
                  });
                  transceiver.associatedRemoteMediaStreams = [];
                }

                transceiver.localCapabilities = localCapabilities;
                transceiver.remoteCapabilities = remoteCapabilities;
                transceiver.rtpReceiver = rtpReceiver;
                transceiver.rtcpParameters = rtcpParameters;
                transceiver.sendEncodingParameters = sendEncodingParameters;
                transceiver.recvEncodingParameters = recvEncodingParameters;

                // Start the RTCRtpReceiver now. The RTPSender is started in
                // setLocalDescription.
                pc._transceive(pc.transceivers[sdpMLineIndex],
                  false,
                  isNewTrack);
              } else if (description.type === 'answer' && !rejected) {
                transceiver = pc.transceivers[sdpMLineIndex];
                iceGatherer = transceiver.iceGatherer;
                iceTransport = transceiver.iceTransport;
                dtlsTransport = transceiver.dtlsTransport;
                rtpReceiver = transceiver.rtpReceiver;
                // sendEncodingParameters = transceiver.sendEncodingParameters;
                // localCapabilities = transceiver.localCapabilities;

                pc.transceivers[sdpMLineIndex].recvEncodingParameters =
                  recvEncodingParameters;
                pc.transceivers[sdpMLineIndex].remoteCapabilities =
                  remoteCapabilities;
                pc.transceivers[sdpMLineIndex].rtcpParameters = rtcpParameters;

                if (cands.length && iceTransport.state === 'new') {
                  if ((isIceLite || isComplete) &&
                    (!usingBundle || sdpMLineIndex === 0)) {
                    iceTransport.setRemoteCandidates(cands);
                  } else {
                    cands.forEach(function (candidate) {
                      maybeAddCandidate(transceiver.iceTransport, candidate);
                    });
                  }
                }

                if (!usingBundle || sdpMLineIndex === 0) {
                  if (iceTransport.state === 'new') {
                    iceTransport.start(iceGatherer, remoteIceParameters,
                      'controlling');
                  }
                  if (dtlsTransport.state === 'new') {
                    dtlsTransport.start(remoteDtlsParameters);
                  }
                }

                // If the offer contained RTX but the answer did not,
                // remove RTX from sendEncodingParameters.
                let commonCapabilities = getCommonCapabilities(
                  transceiver.localCapabilities,
                  transceiver.remoteCapabilities);

                let hasRtx = commonCapabilities.codecs.filter(function (c) {
                  return c.name.toLowerCase() === 'rtx';
                }).length;
                if (!hasRtx && transceiver.sendEncodingParameters[0].rtx) {
                  delete transceiver.sendEncodingParameters[0].rtx;
                }

                pc._transceive(transceiver,
                  direction === 'sendrecv' || direction === 'recvonly',
                  direction === 'sendrecv' || direction === 'sendonly');

                // TO DO: rewrite to use http://w3c.github.io/webrtc-pc/#set-associated-remote-streams
                if (rtpReceiver &&
                  (direction === 'sendrecv' || direction === 'sendonly')) {
                  track = rtpReceiver.track;
                  if (remoteMsid) {
                    if (!streams[remoteMsid.stream]) {
                      streams[remoteMsid.stream] = new window.MediaStream();
                    }
                    addTrackToStreamAndFireEvent(track, streams[remoteMsid.stream]);
                    receiverList.push([track, rtpReceiver, streams[remoteMsid.stream]]);
                  } else {
                    if (!streams.default) {
                      streams.default = new window.MediaStream();
                    }
                    addTrackToStreamAndFireEvent(track, streams.default);
                    receiverList.push([track, rtpReceiver, streams.default]);
                  }
                } else {
                  // FIX ME: actually the receiver should be created later.
                  delete transceiver.rtpReceiver;
                }
              }
            });

            if (pc._dtlsRole === undefined) {
              pc._dtlsRole = description.type === 'offer' ? 'active' : 'passive';
            }

            pc._remoteDescription = {
              type: description.type,
              sdp: description.sdp
            };
            if (description.type === 'offer') {
              pc._updateSignalingState('have-remote-offer');
            } else {
              pc._updateSignalingState('stable');
            }
            Object.keys(streams).forEach(function (sid) {
              let stream = streams[sid];
              if (stream.getTracks().length) {
                if (pc.remoteStreams.indexOf(stream) === -1) {
                  pc.remoteStreams.push(stream);
                  let event = new Event('addstream');
                  event.stream = stream;
                  window.setTimeout(function () {
                    pc._dispatchEvent('addstream', event);
                  });
                }

                receiverList.forEach(function (item) {
                  let track = item[0];
                  let receiver = item[1];
                  if (stream.id !== item[2].id) {
                    return;
                  }
                  fireAddTrack(pc, track, receiver, [stream]);
                });
              }
            });
            receiverList.forEach(function (item) {
              if (item[2]) {
                return;
              }
              fireAddTrack(pc, item[0], item[1], []);
            });

            // check whether addIceCandidate({}) was called within four seconds after
            // setRemoteDescription.
            window.setTimeout(function () {
              if (!(pc && pc.transceivers)) {
                return;
              }
              pc.transceivers.forEach(function (transceiver) {
                if (transceiver.iceTransport &&
                  transceiver.iceTransport.state === 'new' &&
                  transceiver.iceTransport.getRemoteCandidates().length > 0) {
                  console.warn('Timeout for addRemoteCandidate. Consider sending ' +
                    'an end-of-candidates notification');
                  transceiver.iceTransport.addRemoteCandidate({});
                }
              });
            }, 4000);

            return Promise.resolve();
          };

          RTCPeerConnection.prototype.close = function () {
            this.transceivers.forEach(function (transceiver) {
              /* not yet
      if (transceiver.iceGatherer) {
        transceiver.iceGatherer.close();
      }
      */
              if (transceiver.iceTransport) {
                transceiver.iceTransport.stop();
              }
              if (transceiver.dtlsTransport) {
                transceiver.dtlsTransport.stop();
              }
              if (transceiver.rtpSender) {
                transceiver.rtpSender.stop();
              }
              if (transceiver.rtpReceiver) {
                transceiver.rtpReceiver.stop();
              }
            });
            // FIX ME: clean up tracks, local streams, remote streams, etc
            this._isClosed = true;
            this._updateSignalingState('closed');
          };

          // Update the signaling state.
          RTCPeerConnection.prototype._updateSignalingState = function (newState) {
            this.signalingState = newState;
            let event = new Event('signalingstatechange');
            this._dispatchEvent('signalingstatechange', event);
          };

          // Determine whether to fire the negotiationneeded event.
          RTCPeerConnection.prototype._maybeFireNegotiationNeeded = function () {
            let pc = this;
            if (this.signalingState !== 'stable' || this.needNegotiation === true) {
              return;
            }
            // this.needNegotiation = true;
            window.setTimeout(function () {
              if (pc.needNegotiation) {
                pc.needNegotiation = false;
                let event = new Event('negotiationneeded');
                pc._dispatchEvent('negotiationneeded', event);
              }
            }, 0);
          };

          // Update the ice connection state.
          RTCPeerConnection.prototype._updateIceConnectionState = function () {
            let newState;
            let states = {
              'new': 0,
              closed: 0,
              checking: 0,
              connected: 0,
              completed: 0,
              disconnected: 0,
              failed: 0
            };
            this.transceivers.forEach(function (transceiver) {
              if (transceiver.iceTransport && !transceiver.rejected) {
                states[transceiver.iceTransport.state]++;
              }
            });

            newState = 'new';
            if (states.failed > 0) {
              newState = 'failed';
            } else if (states.checking > 0) {
              newState = 'checking';
            } else if (states.disconnected > 0) {
              newState = 'disconnected';
            } else if (states.new > 0) {
              newState = 'new';
            } else if (states.connected > 0) {
              newState = 'connected';
            } else if (states.completed > 0) {
              newState = 'completed';
            }

            if (newState !== this.iceConnectionState) {
              this.iceConnectionState = newState;
              let event = new Event('iceconnectionstatechange');
              this._dispatchEvent('iceconnectionstatechange', event);
            }
          };

          // Update the connection state.
          RTCPeerConnection.prototype._updateConnectionState = function () {
            let newState;
            let states = {
              'new': 0,
              closed: 0,
              connecting: 0,
              connected: 0,
              completed: 0,
              disconnected: 0,
              failed: 0
            };
            this.transceivers.forEach(function (transceiver) {
              if (transceiver.iceTransport && transceiver.dtlsTransport &&
                !transceiver.rejected) {
                states[transceiver.iceTransport.state]++;
                states[transceiver.dtlsTransport.state]++;
              }
            });
            // ICETransport.completed and connected are the same for this purpose.
            states.connected += states.completed;

            newState = 'new';
            if (states.failed > 0) {
              newState = 'failed';
            } else if (states.connecting > 0) {
              newState = 'connecting';
            } else if (states.disconnected > 0) {
              newState = 'disconnected';
            } else if (states.new > 0) {
              newState = 'new';
            } else if (states.connected > 0) {
              newState = 'connected';
            }

            if (newState !== this.connectionState) {
              this.connectionState = newState;
              let event = new Event('connectionstatechange');
              this._dispatchEvent('connectionstatechange', event);
            }
          };

          RTCPeerConnection.prototype.createOffer = function () {
            let pc = this;

            if (pc._isClosed) {
              return Promise.reject(makeError('InvalidStateError',
                'Can not call createOffer after close'));
            }

            let numAudioTracks = pc.transceivers.filter(function (t) {
              return t.kind === 'audio';
            }).length;
            let numVideoTracks = pc.transceivers.filter(function (t) {
              return t.kind === 'video';
            }).length;

            // Determine number of audio and video tracks we need to send/recv.
            let offerOptions = arguments[0];
            if (offerOptions) {
              // Reject Chrome legacy constraints.
              if (offerOptions.mandatory || offerOptions.optional) {
                throw new TypeError(
                  'Legacy mandatory/optional constraints not supported.');
              }
              if (offerOptions.offerToReceiveAudio !== undefined) {
                if (offerOptions.offerToReceiveAudio === true) {
                  numAudioTracks = 1;
                } else if (offerOptions.offerToReceiveAudio === false) {
                  numAudioTracks = 0;
                } else {
                  numAudioTracks = offerOptions.offerToReceiveAudio;
                }
              }
              if (offerOptions.offerToReceiveVideo !== undefined) {
                if (offerOptions.offerToReceiveVideo === true) {
                  numVideoTracks = 1;
                } else if (offerOptions.offerToReceiveVideo === false) {
                  numVideoTracks = 0;
                } else {
                  numVideoTracks = offerOptions.offerToReceiveVideo;
                }
              }
            }

            pc.transceivers.forEach(function (transceiver) {
              if (transceiver.kind === 'audio') {
                numAudioTracks--;
                if (numAudioTracks < 0) {
                  transceiver.wantReceive = false;
                }
              } else if (transceiver.kind === 'video') {
                numVideoTracks--;
                if (numVideoTracks < 0) {
                  transceiver.wantReceive = false;
                }
              }
            });

            // Create M-lines for recvonly streams.
            while (numAudioTracks > 0 || numVideoTracks > 0) {
              if (numAudioTracks > 0) {
                pc._createTransceiver('audio');
                numAudioTracks--;
              }
              if (numVideoTracks > 0) {
                pc._createTransceiver('video');
                numVideoTracks--;
              }
            }

            let sdp = SDPUtils.writeSessionBoilerplate(pc._sdpSessionId,
              pc._sdpSessionVersion++);
            pc.transceivers.forEach(function (transceiver, sdpMLineIndex) {
              // For each track, create an ice gatherer, ice transport,
              // dtls transport, potentially rtpsender and rtpreceiver.
              let track = transceiver.track;
              let kind = transceiver.kind;
              let mid = transceiver.mid || SDPUtils.generateIdentifier();
              transceiver.mid = mid;

              if (!transceiver.iceGatherer) {
                transceiver.iceGatherer = pc._createIceGatherer(sdpMLineIndex,
                  pc.usingBundle);
              }

              let localCapabilities = window.RTCRtpSender.getCapabilities(kind);
              // filter RTX until additional stuff needed for RTX is implemented
              // in adapter.js
              if (edgeVersion < 15019) {
                localCapabilities.codecs = localCapabilities.codecs.filter(
                  function (codec) {
                    return codec.name !== 'rtx';
                  });
              }
              localCapabilities.codecs.forEach(function (codec) {
                // work around https://bugs.chromium.org/p/webrtc/issues/detail?id=6552
                // by adding level-asymmetry-allowed=1
                if (codec.name === 'H264' &&
                  codec.parameters['level-asymmetry-allowed'] === undefined) {
                  codec.parameters['level-asymmetry-allowed'] = '1';
                }

                // for subsequent offers, we might have to re-use the payload
                // type of the last offer.
                if (transceiver.remoteCapabilities &&
                  transceiver.remoteCapabilities.codecs) {
                  transceiver.remoteCapabilities.codecs.forEach(function (remoteCodec) {
                    if (codec.name.toLowerCase() === remoteCodec.name.toLowerCase() &&
                      codec.clockRate === remoteCodec.clockRate) {
                      codec.preferredPayloadType = remoteCodec.payloadType;
                    }
                  });
                }
              });
              localCapabilities.headerExtensions.forEach(function (hdrExt) {
                let remoteExtensions = transceiver.remoteCapabilities &&
                  transceiver.remoteCapabilities.headerExtensions || [];
                remoteExtensions.forEach(function (rHdrExt) {
                  if (hdrExt.uri === rHdrExt.uri) {
                    hdrExt.id = rHdrExt.id;
                  }
                });
              });

              // generate an ssrc now, to be used later in rtpSender.send
              let sendEncodingParameters = transceiver.sendEncodingParameters || [{
                ssrc: (2 * sdpMLineIndex + 1) * 1001
              }];
              if (track) {
                // add RTX
                if (edgeVersion >= 15019 && kind === 'video' &&
                  !sendEncodingParameters[0].rtx) {
                  sendEncodingParameters[0].rtx = {
                    ssrc: sendEncodingParameters[0].ssrc + 1
                  };
                }
              }

              if (transceiver.wantReceive) {
                transceiver.rtpReceiver = new window.RTCRtpReceiver(
                  transceiver.dtlsTransport, kind);
              }

              transceiver.localCapabilities = localCapabilities;
              transceiver.sendEncodingParameters = sendEncodingParameters;
            });

            // always offer BUNDLE and dispose on return if not supported.
            if (pc._config.bundlePolicy !== 'max-compat') {
              sdp += 'a=group:BUNDLE ' + pc.transceivers.map(function (t) {
                return t.mid;
              }).join(' ') + '\r\n';
            }
            sdp += 'a=ice-options:trickle\r\n';

            pc.transceivers.forEach(function (transceiver, sdpMLineIndex) {
              sdp += writeMediaSection(transceiver, transceiver.localCapabilities,
                'offer', transceiver.stream, pc._dtlsRole);
              sdp += 'a=rtcp-rsize\r\n';

              if (transceiver.iceGatherer && pc.iceGatheringState !== 'new' &&
                (sdpMLineIndex === 0 || !pc.usingBundle)) {
                transceiver.iceGatherer.getLocalCandidates().forEach(function (cand) {
                  cand.component = 1;
                  sdp += 'a=' + SDPUtils.writeCandidate(cand) + '\r\n';
                });

                if (transceiver.iceGatherer.state === 'completed') {
                  sdp += 'a=end-of-candidates\r\n';
                }
              }
            });

            let desc = new window.RTCSessionDescription({
              type: 'offer',
              sdp: sdp
            });
            return Promise.resolve(desc);
          };

          RTCPeerConnection.prototype.createAnswer = function () {
            let pc = this;

            if (pc._isClosed) {
              return Promise.reject(makeError('InvalidStateError',
                'Can not call createAnswer after close'));
            }

            if (!(pc.signalingState === 'have-remote-offer' ||
              pc.signalingState === 'have-local-pranswer')) {
              return Promise.reject(makeError('InvalidStateError',
                'Can not call createAnswer in signalingState ' + pc.signalingState));
            }

            let sdp = SDPUtils.writeSessionBoilerplate(pc._sdpSessionId,
              pc._sdpSessionVersion++);
            if (pc.usingBundle) {
              sdp += 'a=group:BUNDLE ' + pc.transceivers.map(function (t) {
                return t.mid;
              }).join(' ') + '\r\n';
            }
            sdp += 'a=ice-options:trickle\r\n';

            let mediaSectionsInOffer = SDPUtils.getMediaSections(
              pc._remoteDescription.sdp).length;
            pc.transceivers.forEach(function (transceiver, sdpMLineIndex) {
              if (sdpMLineIndex + 1 > mediaSectionsInOffer) {
                return;
              }
              if (transceiver.rejected) {
                if (transceiver.kind === 'application') {
                  if (transceiver.protocol === 'DTLS/SCTP') { // legacy fmt
                    sdp += 'm=application 0 DTLS/SCTP 5000\r\n';
                  } else {
                    sdp += 'm=application 0 ' + transceiver.protocol +
                      ' webrtc-datachannel\r\n';
                  }
                } else if (transceiver.kind === 'audio') {
                  sdp += 'm=audio 0 UDP/TLS/RTP/SAVPF 0\r\n' +
                    'a=rtpmap:0 PCMU/8000\r\n';
                } else if (transceiver.kind === 'video') {
                  sdp += 'm=video 0 UDP/TLS/RTP/SAVPF 120\r\n' +
                    'a=rtpmap:120 VP8/90000\r\n';
                }
                sdp += 'c=IN IP4 0.0.0.0\r\n' +
                  'a=inactive\r\n' +
                  'a=mid:' + transceiver.mid + '\r\n';
                return;
              }

              // FIX ME: look at direction.
              if (transceiver.stream) {
                let localTrack;
                if (transceiver.kind === 'audio') {
                  localTrack = transceiver.stream.getAudioTracks()[0];
                } else if (transceiver.kind === 'video') {
                  localTrack = transceiver.stream.getVideoTracks()[0];
                }
                if (localTrack) {
                  // add RTX
                  if (edgeVersion >= 15019 && transceiver.kind === 'video' &&
                    !transceiver.sendEncodingParameters[0].rtx) {
                    transceiver.sendEncodingParameters[0].rtx = {
                      ssrc: transceiver.sendEncodingParameters[0].ssrc + 1
                    };
                  }
                }
              }

              // Calculate intersection of capabilities.
              let commonCapabilities = getCommonCapabilities(
                transceiver.localCapabilities,
                transceiver.remoteCapabilities);

              let hasRtx = commonCapabilities.codecs.filter(function (c) {
                return c.name.toLowerCase() === 'rtx';
              }).length;
              if (!hasRtx && transceiver.sendEncodingParameters[0].rtx) {
                delete transceiver.sendEncodingParameters[0].rtx;
              }

              sdp += writeMediaSection(transceiver, commonCapabilities,
                'answer', transceiver.stream, pc._dtlsRole);
              if (transceiver.rtcpParameters &&
                transceiver.rtcpParameters.reducedSize) {
                sdp += 'a=rtcp-rsize\r\n';
              }
            });

            let desc = new window.RTCSessionDescription({
              type: 'answer',
              sdp: sdp
            });
            return Promise.resolve(desc);
          };

          RTCPeerConnection.prototype.addIceCandidate = function (candidate) {
            let pc = this;
            let sections;
            if (candidate && !(candidate.sdpMLineIndex !== undefined ||
              candidate.sdpMid)) {
              return Promise.reject(new TypeError('sdpMLineIndex or sdpMid required'));
            }

            // TO DO: needs to go into ops queue.
            return new Promise(function (resolve, reject) {
              if (!pc._remoteDescription) {
                return reject(makeError('InvalidStateError',
                  'Can not add ICE candidate without a remote description'));
              } else if (!candidate || candidate.candidate === '') {
                for (let j = 0; j < pc.transceivers.length; j++) {
                  if (pc.transceivers[j].rejected) {
                    continue;
                  }
                  pc.transceivers[j].iceTransport.addRemoteCandidate({});
                  sections = SDPUtils.getMediaSections(pc._remoteDescription.sdp);
                  sections[j] += 'a=end-of-candidates\r\n';
                  pc._remoteDescription.sdp =
                    SDPUtils.getDescription(pc._remoteDescription.sdp) +
                    sections.join('');
                  if (pc.usingBundle) {
                    break;
                  }
                }
              } else {
                let sdpMLineIndex = candidate.sdpMLineIndex;
                if (candidate.sdpMid) {
                  for (let i = 0; i < pc.transceivers.length; i++) {
                    if (pc.transceivers[i].mid === candidate.sdpMid) {
                      sdpMLineIndex = i;
                      break;
                    }
                  }
                }
                let transceiver = pc.transceivers[sdpMLineIndex];
                if (transceiver) {
                  if (transceiver.rejected) {
                    return resolve();
                  }
                  let cand = Object.keys(candidate.candidate).length > 0 ?
                    SDPUtils.parseCandidate(candidate.candidate) : {};
                  // Ignore Chrome's invalid candidates since Edge does not like them.
                  if (cand.protocol === 'tcp' && (cand.port === 0 || cand.port === 9)) {
                    return resolve();
                  }
                  // Ignore RTCP candidates, we assume RTCP-MUX.
                  if (cand.component && cand.component !== 1) {
                    return resolve();
                  }
                  // when using bundle, avoid adding candidates to the wrong
                  // ice transport. And avoid adding candidates added in the SDP.
                  if (sdpMLineIndex === 0 || (sdpMLineIndex > 0 &&
                    transceiver.iceTransport !== pc.transceivers[0].iceTransport)) {
                    if (!maybeAddCandidate(transceiver.iceTransport, cand)) {
                      return reject(makeError('OperationError',
                        'Can not add ICE candidate'));
                    }
                  }

                  // update the remoteDescription.
                  let candidateString = candidate.candidate.trim();
                  if (candidateString.indexOf('a=') === 0) {
                    candidateString = candidateString.substr(2);
                  }
                  sections = SDPUtils.getMediaSections(pc._remoteDescription.sdp);
                  sections[sdpMLineIndex] += 'a=' +
                    (cand.type ? candidateString : 'end-of-candidates')
                    + '\r\n';
                  pc._remoteDescription.sdp =
                    SDPUtils.getDescription(pc._remoteDescription.sdp) +
                    sections.join('');
                } else {
                  return reject(makeError('OperationError',
                    'Can not add ICE candidate'));
                }
              }
              resolve();
            });
          };

          RTCPeerConnection.prototype.getStats = function (selector) {
            if (selector && selector instanceof window.MediaStreamTrack) {
              let senderOrReceiver = null;
              this.transceivers.forEach(function (transceiver) {
                if (transceiver.rtpSender &&
                  transceiver.rtpSender.track === selector) {
                  senderOrReceiver = transceiver.rtpSender;
                } else if (transceiver.rtpReceiver &&
                  transceiver.rtpReceiver.track === selector) {
                  senderOrReceiver = transceiver.rtpReceiver;
                }
              });
              if (!senderOrReceiver) {
                throw makeError('InvalidAccessError', 'Invalid selector.');
              }
              return senderOrReceiver.getStats();
            }

            let promises = [];
            this.transceivers.forEach(function (transceiver) {
              ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport',
                'dtlsTransport'].forEach(function (method) {
                if (transceiver[method]) {
                  promises.push(transceiver[method].getStats());
                }
              });
            });
            return Promise.all(promises).then(function (allStats) {
              let results = new Map();
              allStats.forEach(function (stats) {
                stats.forEach(function (stat) {
                  results.set(stat.id, stat);
                });
              });
              return results;
            });
          };

          // fix low-level stat names and return Map instead of object.
          let ortcObjects = ['RTCRtpSender', 'RTCRtpReceiver', 'RTCIceGatherer',
            'RTCIceTransport', 'RTCDtlsTransport'];
          ortcObjects.forEach(function (ortcObjectName) {
            let obj = window[ortcObjectName];
            if (obj && obj.prototype && obj.prototype.getStats) {
              let nativeGetstats = obj.prototype.getStats;
              obj.prototype.getStats = function () {
                return nativeGetstats.apply(this)
                  .then(function (nativeStats) {
                    let mapStats = new Map();
                    Object.keys(nativeStats).forEach(function (id) {
                      nativeStats[id].type = fixStatsType(nativeStats[id]);
                      mapStats.set(id, nativeStats[id]);
                    });
                    return mapStats;
                  });
              };
            }
          });

          // legacy callback shims. Should be moved to adapter.js some days.
          let methods = ['createOffer', 'createAnswer'];
          methods.forEach(function (method) {
            let nativeMethod = RTCPeerConnection.prototype[method];
            RTCPeerConnection.prototype[method] = function () {
              let args = arguments;
              if (typeof args[0] === 'function' ||
                typeof args[1] === 'function') { // legacy
                return nativeMethod.apply(this, [arguments[2]])
                  .then(function (description) {
                    if (typeof args[0] === 'function') {
                      args[0].apply(null, [description]);
                    }
                  }, function (error) {
                    if (typeof args[1] === 'function') {
                      args[1].apply(null, [error]);
                    }
                  });
              }
              return nativeMethod.apply(this, arguments);
            };
          });

          methods = ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'];
          methods.forEach(function (method) {
            let nativeMethod = RTCPeerConnection.prototype[method];
            RTCPeerConnection.prototype[method] = function () {
              let args = arguments;
              if (typeof args[1] === 'function' ||
                typeof args[2] === 'function') { // legacy
                return nativeMethod.apply(this, arguments)
                  .then(function () {
                    if (typeof args[1] === 'function') {
                      args[1].apply(null);
                    }
                  }, function (error) {
                    if (typeof args[2] === 'function') {
                      args[2].apply(null, [error]);
                    }
                  });
              }
              return nativeMethod.apply(this, arguments);
            };
          });

          // getStats is special. It doesn't have a spec legacy method yet we support
          // getStats(something, cb) without error callbacks.
          ['getStats'].forEach(function (method) {
            let nativeMethod = RTCPeerConnection.prototype[method];
            RTCPeerConnection.prototype[method] = function () {
              let args = arguments;
              if (typeof args[1] === 'function') {
                return nativeMethod.apply(this, arguments)
                  .then(function () {
                    if (typeof args[1] === 'function') {
                      args[1].apply(null);
                    }
                  });
              }
              return nativeMethod.apply(this, arguments);
            };
          });

          return RTCPeerConnection;
        };

      }, {"sdp": 17}], 17: [function (require, module, exports) {
        /* eslint-env node */
        'use strict';

// SDP helpers.
        let SDPUtils = {};

// Generate an alphanumeric identifier for cname or mids.
// TO DO: use UUIDs instead? https://gist.github.com/jed/982883
        SDPUtils.generateIdentifier = function () {
          return Math.random().toString(36).substr(2, 10);
        };

// The RTCP CNAME used by all peerconnections from the same JS.
        SDPUtils.localCName = SDPUtils.generateIdentifier();

// Splits SDP into lines, dealing with both CRLF and LF.
        SDPUtils.splitLines = function (blob) {
          return blob.trim().split('\n').map(function (line) {
            return line.trim();
          });
        };
// Splits SDP into sessionpart and mediasections. Ensures CRLF.
        SDPUtils.splitSections = function (blob) {
          let parts = blob.split('\nm=');
          return parts.map(function (part, index) {
            return (index > 0 ? 'm=' + part : part).trim() + '\r\n';
          });
        };

// returns the session description.
        SDPUtils.getDescription = function (blob) {
          let sections = SDPUtils.splitSections(blob);
          return sections && sections[0];
        };

// returns the individual media sections.
        SDPUtils.getMediaSections = function (blob) {
          let sections = SDPUtils.splitSections(blob);
          sections.shift();
          return sections;
        };

// Returns lines that start with a certain prefix.
        SDPUtils.matchPrefix = function (blob, prefix) {
          return SDPUtils.splitLines(blob).filter(function (line) {
            return line.indexOf(prefix) === 0;
          });
        };

// Parses an ICE candidate line. Sample input:
// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8
// rport 55996"
        SDPUtils.parseCandidate = function (line) {
          let parts;
          // Parse both variants.
          if (line.indexOf('a=candidate:') === 0) {
            parts = line.substring(12).split(' ');
          } else {
            parts = line.substring(10).split(' ');
          }

          let candidate = {
            foundation: parts[0],
            component: parseInt(parts[1], 10),
            protocol: parts[2].toLowerCase(),
            priority: parseInt(parts[3], 10),
            ip: parts[4],
            address: parts[4], // address is an alias for ip.
            port: parseInt(parts[5], 10),
            // skip parts[6] == 'typ'
            type: parts[7]
          };

          for (let i = 8; i < parts.length; i += 2) {
            switch (parts[i]) {
              case 'raddr':
                candidate.relatedAddress = parts[i + 1];
                break;
              case 'rport':
                candidate.relatedPort = parseInt(parts[i + 1], 10);
                break;
              case 'tcptype':
                candidate.tcpType = parts[i + 1];
                break;
              case 'ufrag':
                candidate.ufrag = parts[i + 1]; // for backward compability.
                candidate.usernameFragment = parts[i + 1];
                break;
              default: // extension handling, in particular ufrag
                candidate[parts[i]] = parts[i + 1];
                break;
            }
          }
          return candidate;
        };

// Translates a candidate object into SDP candidate attribute.
        SDPUtils.writeCandidate = function (candidate) {
          let sdp = [];
          sdp.push(candidate.foundation);
          sdp.push(candidate.component);
          sdp.push(candidate.protocol.toUpperCase());
          sdp.push(candidate.priority);
          sdp.push(candidate.address || candidate.ip);
          sdp.push(candidate.port);

          let type = candidate.type;
          sdp.push('typ');
          sdp.push(type);
          if (type !== 'host' && candidate.relatedAddress &&
            candidate.relatedPort) {
            sdp.push('raddr');
            sdp.push(candidate.relatedAddress);
            sdp.push('rport');
            sdp.push(candidate.relatedPort);
          }
          if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') {
            sdp.push('tcptype');
            sdp.push(candidate.tcpType);
          }
          if (candidate.usernameFragment || candidate.ufrag) {
            sdp.push('ufrag');
            sdp.push(candidate.usernameFragment || candidate.ufrag);
          }
          return 'candidate:' + sdp.join(' ');
        };

// Parses an ice-options line, returns an array of option tags.
// a=ice-options:foo bar
        SDPUtils.parseIceOptions = function (line) {
          return line.substr(14).split(' ');
        };

// Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input:
// a=rtpmap:111 opus/48000/2
        SDPUtils.parseRtpMap = function (line) {
          let parts = line.substr(9).split(' ');
          let parsed = {
            payloadType: parseInt(parts.shift(), 10) // was: id
          };

          parts = parts[0].split('/');

          parsed.name = parts[0];
          parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
          parsed.channels = parts.length === 3 ? parseInt(parts[2], 10) : 1;
          // legacy alias, got renamed back to channels in ORTC.
          parsed.numChannels = parsed.channels;
          return parsed;
        };

// Generate an a=rtpmap line from RTCRtpCodecCapability or
// RTCRtpCodecParameters.
        SDPUtils.writeRtpMap = function (codec) {
          let pt = codec.payloadType;
          if (codec.preferredPayloadType !== undefined) {
            pt = codec.preferredPayloadType;
          }
          let channels = codec.channels || codec.numChannels || 1;
          return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate +
            (channels !== 1 ? '/' + channels : '') + '\r\n';
        };

// Parses an a=extmap line (headerextension from RFC 5285). Sample input:
// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset
        SDPUtils.parseExtmap = function (line) {
          let parts = line.substr(9).split(' ');
          return {
            id: parseInt(parts[0], 10),
            direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv',
            uri: parts[1]
          };
        };

// Generates a=extmap line from RTCRtpHeaderExtensionParameters or
// RTCRtpHeaderExtension.
        SDPUtils.writeExtmap = function (headerExtension) {
          return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) +
            (headerExtension.direction && headerExtension.direction !== 'sendrecv'
              ? '/' + headerExtension.direction
              : '') +
            ' ' + headerExtension.uri + '\r\n';
        };

// Parses an ftmp line, returns dictionary. Sample input:
// a=fmtp:96 vbr=on;cng=on
// Also deals with vbr=on; cng=on
        SDPUtils.parseFmtp = function (line) {
          let parsed = {};
          let kv;
          let parts = line.substr(line.indexOf(' ') + 1).split(';');
          for (let j = 0; j < parts.length; j++) {
            kv = parts[j].trim().split('=');
            parsed[kv[0].trim()] = kv[1];
          }
          return parsed;
        };

// Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters.
        SDPUtils.writeFmtp = function (codec) {
          let line = '';
          let pt = codec.payloadType;
          if (codec.preferredPayloadType !== undefined) {
            pt = codec.preferredPayloadType;
          }
          if (codec.parameters && Object.keys(codec.parameters).length) {
            let params = [];
            Object.keys(codec.parameters).forEach(function (param) {
              if (codec.parameters[param]) {
                params.push(param + '=' + codec.parameters[param]);
              } else {
                params.push(param);
              }
            });
            line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n';
          }
          return line;
        };

// Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input:
// a=rtcp-fb:98 nack rpsi
        SDPUtils.parseRtcpFb = function (line) {
          let parts = line.substr(line.indexOf(' ') + 1).split(' ');
          return {
            type: parts.shift(),
            parameter: parts.join(' ')
          };
        };
// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters.
        SDPUtils.writeRtcpFb = function (codec) {
          let lines = '';
          let pt = codec.payloadType;
          if (codec.preferredPayloadType !== undefined) {
            pt = codec.preferredPayloadType;
          }
          if (codec.rtcpFeedback && codec.rtcpFeedback.length) {
            // FIX ME: special handling for trr-int?
            codec.rtcpFeedback.forEach(function (fb) {
              lines += 'a=rtcp-fb:' + pt + ' ' + fb.type +
                (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') +
                '\r\n';
            });
          }
          return lines;
        };

// Parses an RFC 5576 ssrc media attribute. Sample input:
// a=ssrc:3735928559 cname:something
        SDPUtils.parseSsrcMedia = function (line) {
          let sp = line.indexOf(' ');
          let parts = {
            ssrc: parseInt(line.substr(7, sp - 7), 10)
          };
          let colon = line.indexOf(':', sp);
          if (colon > -1) {
            parts.attribute = line.substr(sp + 1, colon - sp - 1);
            parts.value = line.substr(colon + 1);
          } else {
            parts.attribute = line.substr(sp + 1);
          }
          return parts;
        };

        SDPUtils.parseSsrcGroup = function (line) {
          let parts = line.substr(13).split(' ');
          return {
            semantics: parts.shift(),
            ssrcs: parts.map(function (ssrc) {
              return parseInt(ssrc, 10);
            })
          };
        };

// Extracts the MID (RFC 5888) from a media section.
// returns the MID or undefined if no mid line was found.
        SDPUtils.getMid = function (mediaSection) {
          let mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0];
          if (mid) {
            return mid.substr(6);
          }
        };

        SDPUtils.parseFingerprint = function (line) {
          let parts = line.substr(14).split(' ');
          return {
            algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge.
            value: parts[1]
          };
        };

// Extracts DTLS parameters from SDP media section or sessionpart.
// FIX ME: for consistency with other functions this should only
//   get the fingerprint line as input. See also getIceParameters.
        SDPUtils.getDtlsParameters = function (mediaSection, sessionpart) {
          let lines = SDPUtils.matchPrefix(mediaSection + sessionpart,
            'a=fingerprint:');
          // Note: a=setup line is ignored since we use the 'auto' role.
          // Note2: 'algorithm' is not case sensitive except in Edge.
          return {
            role: 'auto',
            fingerprints: lines.map(SDPUtils.parseFingerprint)
          };
        };

// Serializes DTLS parameters to SDP.
        SDPUtils.writeDtlsParameters = function (params, setupType) {
          let sdp = 'a=setup:' + setupType + '\r\n';
          params.fingerprints.forEach(function (fp) {
            sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n';
          });
          return sdp;
        };

// Parses a=crypto lines into
//   https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#dictionary-rtcsrtpsdesparameters-members
        SDPUtils.parseCryptoLine = function (line) {
          let parts = line.substr(9).split(' ');
          return {
            tag: parseInt(parts[0], 10),
            cryptoSuite: parts[1],
            keyParams: parts[2],
            sessionParams: parts.slice(3),
          };
        };

        SDPUtils.writeCryptoLine = function (parameters) {
          return 'a=crypto:' + parameters.tag + ' ' +
            parameters.cryptoSuite + ' ' +
            (typeof parameters.keyParams === 'object'
              ? SDPUtils.writeCryptoKeyParams(parameters.keyParams)
              : parameters.keyParams) +
            (parameters.sessionParams ? ' ' + parameters.sessionParams.join(' ') : '') +
            '\r\n';
        };

// Parses the crypto key parameters into
//   https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#rtcsrtpkeyparam*
        SDPUtils.parseCryptoKeyParams = function (keyParams) {
          if (keyParams.indexOf('inline:') !== 0) {
            return null;
          }
          let parts = keyParams.substr(7).split('|');
          return {
            keyMethod: 'inline',
            keySalt: parts[0],
            lifeTime: parts[1],
            mkiValue: parts[2] ? parts[2].split(':')[0] : undefined,
            mkiLength: parts[2] ? parts[2].split(':')[1] : undefined,
          };
        };

        SDPUtils.writeCryptoKeyParams = function (keyParams) {
          return keyParams.keyMethod + ':'
            + keyParams.keySalt +
            (keyParams.lifeTime ? '|' + keyParams.lifeTime : '') +
            (keyParams.mkiValue && keyParams.mkiLength
              ? '|' + keyParams.mkiValue + ':' + keyParams.mkiLength
              : '');
        };

// Extracts all SDES paramters.
        SDPUtils.getCryptoParameters = function (mediaSection, sessionpart) {
          let lines = SDPUtils.matchPrefix(mediaSection + sessionpart,
            'a=crypto:');
          return lines.map(SDPUtils.parseCryptoLine);
        };

// Parses ICE information from SDP media section or sessionpart.
// FIX ME: for consistency with other functions this should only
//   get the ice-ufrag and ice-pwd lines as input.
        SDPUtils.getIceParameters = function (mediaSection, sessionpart) {
          let ufrag = SDPUtils.matchPrefix(mediaSection + sessionpart,
            'a=ice-ufrag:')[0];
          let pwd = SDPUtils.matchPrefix(mediaSection + sessionpart,
            'a=ice-pwd:')[0];
          if (!(ufrag && pwd)) {
            return null;
          }
          return {
            usernameFragment: ufrag.substr(12),
            password: pwd.substr(10),
          };
        };

// Serializes ICE parameters to SDP.
        SDPUtils.writeIceParameters = function (params) {
          return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' +
            'a=ice-pwd:' + params.password + '\r\n';
        };

// Parses the SDP media section and returns RTCRtpParameters.
        SDPUtils.parseRtpParameters = function (mediaSection) {
          let description = {
            codecs: [],
            headerExtensions: [],
            fecMechanisms: [],
            rtcp: []
          };
          let lines = SDPUtils.splitLines(mediaSection);
          let mline = lines[0].split(' ');
          for (let i = 3; i < mline.length; i++) { // find all codecs from mline[3..]
            let pt = mline[i];
            let rtpmapline = SDPUtils.matchPrefix(
              mediaSection, 'a=rtpmap:' + pt + ' ')[0];
            if (rtpmapline) {
              let codec = SDPUtils.parseRtpMap(rtpmapline);
              let fmtps = SDPUtils.matchPrefix(
                mediaSection, 'a=fmtp:' + pt + ' ');
              // Only the first a=fmtp:<pt> is considered.
              codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {};
              codec.rtcpFeedback = SDPUtils.matchPrefix(
                mediaSection, 'a=rtcp-fb:' + pt + ' ')
                .map(SDPUtils.parseRtcpFb);
              description.codecs.push(codec);
              // parse FEC mechanisms from rtpmap lines.
              switch (codec.name.toUpperCase()) {
                case 'RED':
                case 'ULPFEC':
                  description.fecMechanisms.push(codec.name.toUpperCase());
                  break;
                default: // only RED and ULPFEC are recognized as FEC mechanisms.
                  break;
              }
            }
          }
          SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function (line) {
            description.headerExtensions.push(SDPUtils.parseExtmap(line));
          });
          // FIX ME: parse rtcp.
          return description;
        };

// Generates parts of the SDP media section describing the capabilities /
// parameters.
        SDPUtils.writeRtpDescription = function (kind, caps) {
          let sdp = '';

          // Build the mline.
          sdp += 'm=' + kind + ' ';
          sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs.
          sdp += ' UDP/TLS/RTP/SAVPF ';
          sdp += caps.codecs.map(function (codec) {
            if (codec.preferredPayloadType !== undefined) {
              return codec.preferredPayloadType;
            }
            return codec.payloadType;
          }).join(' ') + '\r\n';

          sdp += 'c=IN IP4 0.0.0.0\r\n';
          sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';

          // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
          caps.codecs.forEach(function (codec) {
            sdp += SDPUtils.writeRtpMap(codec);
            sdp += SDPUtils.writeFmtp(codec);
            sdp += SDPUtils.writeRtcpFb(codec);
          });
          let maxptime = 0;
          caps.codecs.forEach(function (codec) {
            if (codec.maxptime > maxptime) {
              maxptime = codec.maxptime;
            }
          });
          if (maxptime > 0) {
            sdp += 'a=maxptime:' + maxptime + '\r\n';
          }
          sdp += 'a=rtcp-mux\r\n';

          if (caps.headerExtensions) {
            caps.headerExtensions.forEach(function (extension) {
              sdp += SDPUtils.writeExtmap(extension);
            });
          }
          // FIX ME: write fecMechanisms.
          return sdp;
        };

// Parses the SDP media section and returns an array of
// RTCRtpEncodingParameters.
        SDPUtils.parseRtpEncodingParameters = function (mediaSection) {
          let encodingParameters = [];
          let description = SDPUtils.parseRtpParameters(mediaSection);
          let hasRed = description.fecMechanisms.indexOf('RED') !== -1;
          let hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1;

          // filter a=ssrc:... cname:, ignore PlanB-msid
          let ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
            .map(function (line) {
              return SDPUtils.parseSsrcMedia(line);
            })
            .filter(function (parts) {
              return parts.attribute === 'cname';
            });
          let primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc;
          let secondarySsrc;

          let flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID')
            .map(function (line) {
              let parts = line.substr(17).split(' ');
              return parts.map(function (part) {
                return parseInt(part, 10);
              });
            });
          if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) {
            secondarySsrc = flows[0][1];
          }

          description.codecs.forEach(function (codec) {
            codec.parameters.apt = !!codec.parameters.apt ? codec.parameters.apt: true;//add
            if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) {
              let encParam = {
                ssrc: primarySsrc,
                codecPayloadType: parseInt(codec.parameters.apt, 10)
              };
              if (primarySsrc && secondarySsrc) {
                encParam.rtx = {ssrc: secondarySsrc};
              }
              encodingParameters.push(encParam);
              if (hasRed) {
                encParam = JSON.parse(JSON.stringify(encParam));
                encParam.fec = {
                  ssrc: primarySsrc,
                  mechanism: hasUlpfec ? 'red+ulpfec' : 'red'
                };
                encodingParameters.push(encParam);
              }
            }
          });
          if (encodingParameters.length === 0 && primarySsrc) {
            encodingParameters.push({
              ssrc: primarySsrc
            });
          }

          // we support both b=AS and b=TIAS but interpret AS as TIAS.
          let bandwidth = SDPUtils.matchPrefix(mediaSection, 'b=');
          if (bandwidth.length) {
            if (bandwidth[0].indexOf('b=TIAS:') === 0) {
              bandwidth = parseInt(bandwidth[0].substr(7), 10);
            } else if (bandwidth[0].indexOf('b=AS:') === 0) {
              // use formula from JSEP to convert b=AS to TIAS value.
              bandwidth = parseInt(bandwidth[0].substr(5), 10) * 1000 * 0.95
                - (50 * 40 * 8);
            } else {
              bandwidth = undefined;
            }
            encodingParameters.forEach(function (params) {
              params.maxBitrate = bandwidth;
            });
          }
          return encodingParameters;
        };

// parses http://draft.ortc.org/#rtcrtcpparameters*
        SDPUtils.parseRtcpParameters = function (mediaSection) {
          let rtcpParameters = {};

          // Gets the first SSRC. Note tha with RTX there might be multiple
          // SSRCs.
          let remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
            .map(function (line) {
              return SDPUtils.parseSsrcMedia(line);
            })
            .filter(function (obj) {
              return obj.attribute === 'cname';
            })[0];
          if (remoteSsrc) {
            rtcpParameters.cname = remoteSsrc.value;
            rtcpParameters.ssrc = remoteSsrc.ssrc;
          }

          // Edge uses the compound attribute instead of reducedSize
          // compound is !reducedSize
          let rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize');
          rtcpParameters.reducedSize = rsize.length > 0;
          rtcpParameters.compound = rsize.length === 0;

          // parses the rtcp-mux attrіbute.
          // Note that Edge does not support unmuxed RTCP.
          let mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux');
          rtcpParameters.mux = mux.length > 0;

          return rtcpParameters;
        };

// parses either a=msid: or a=ssrc:... msid lines and returns
// the id of the MediaStream and MediaStreamTrack.
        SDPUtils.parseMsid = function (mediaSection) {
          let parts;
          let spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:');
          if (spec.length === 1) {
            parts = spec[0].substr(7).split(' ');
            return {stream: parts[0], track: parts[1]};
          }
          let planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
            .map(function (line) {
              return SDPUtils.parseSsrcMedia(line);
            })
            .filter(function (msidParts) {
              return msidParts.attribute === 'msid';
            });
          if (planB.length > 0) {
            parts = planB[0].value.split(' ');
            return {stream: parts[0], track: parts[1]};
          }
        };

// SCTP
// parses draft-ietf-mmusic-sctp-sdp-26 first and falls back
// to draft-ietf-mmusic-sctp-sdp-05
        SDPUtils.parseSctpDescription = function (mediaSection) {
          let mline = SDPUtils.parseMLine(mediaSection);
          let maxSizeLine = SDPUtils.matchPrefix(mediaSection, 'a=max-message-size:');
          let maxMessageSize;
          if (maxSizeLine.length > 0) {
            maxMessageSize = parseInt(maxSizeLine[0].substr(19), 10);
          }
          if (isNaN(maxMessageSize)) {
            maxMessageSize = 65536;
          }
          let sctpPort = SDPUtils.matchPrefix(mediaSection, 'a=sctp-port:');
          if (sctpPort.length > 0) {
            return {
              port: parseInt(sctpPort[0].substr(12), 10),
              protocol: mline.fmt,
              maxMessageSize: maxMessageSize
            };
          }
          let sctpMapLines = SDPUtils.matchPrefix(mediaSection, 'a=sctpmap:');
          if (sctpMapLines.length > 0) {
            let parts = SDPUtils.matchPrefix(mediaSection, 'a=sctpmap:')[0]
              .substr(10)
              .split(' ');
            return {
              port: parseInt(parts[0], 10),
              protocol: parts[1],
              maxMessageSize: maxMessageSize
            };
          }
        };

// SCTP
// outputs the draft-ietf-mmusic-sctp-sdp-26 version that all browsers
// support by now receiving in this format, unless we originally parsed
// as the draft-ietf-mmusic-sctp-sdp-05 format (indicated by the m-line
// protocol of DTLS/SCTP -- without UDP/ or TCP/)
        SDPUtils.writeSctpDescription = function (media, sctp) {
          let output = [];
          if (media.protocol !== 'DTLS/SCTP') {
            output = [
              'm=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.protocol + '\r\n',
              'c=IN IP4 0.0.0.0\r\n',
              'a=sctp-port:' + sctp.port + '\r\n'
            ];
          } else {
            output = [
              'm=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.port + '\r\n',
              'c=IN IP4 0.0.0.0\r\n',
              'a=sctpmap:' + sctp.port + ' ' + sctp.protocol + ' 65535\r\n'
            ];
          }
          if (sctp.maxMessageSize !== undefined) {
            output.push('a=max-message-size:' + sctp.maxMessageSize + '\r\n');
          }
          return output.join('');
        };

// Generate a session ID for SDP.
// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1
// recommends using a cryptographically random +ve 64-bit value
// but right now this should be acceptable and within the right range
        SDPUtils.generateSessionId = function () {
          return Math.random().toString().substr(2, 21);
        };

// Write boilder plate for start of SDP
// sessId argument is optional - if not supplied it will
// be generated randomly
// sessVersion is optional and defaults to 2
// sessUser is optional and defaults to 'thisisadapterortc'
        SDPUtils.writeSessionBoilerplate = function (sessId, sessVer, sessUser) {
          let sessionId;
          let version = sessVer !== undefined ? sessVer : 2;
          if (sessId) {
            sessionId = sessId;
          } else {
            sessionId = SDPUtils.generateSessionId();
          }
          let user = sessUser || 'thisisadapterortc';
          // FIX ME: sess-id should be an NTP timestamp.
          return 'v=0\r\n' +
            'o=' + user + ' ' + sessionId + ' ' + version +
            ' IN IP4 127.0.0.1\r\n' +
            's=-\r\n' +
            't=0 0\r\n';
        };

        SDPUtils.writeMediaSection = function (transceiver, caps, type, stream) {
          let sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);

          // Map ICE parameters (ufrag, pwd) to SDP.
          sdp += SDPUtils.writeIceParameters(
            transceiver.iceGatherer.getLocalParameters());

          // Map DTLS parameters to SDP.
          sdp += SDPUtils.writeDtlsParameters(
            transceiver.dtlsTransport.getLocalParameters(),
            type === 'offer' ? 'actpass' : 'active');

          sdp += 'a=mid:' + transceiver.mid + '\r\n';

          if (transceiver.direction) {
            sdp += 'a=' + transceiver.direction + '\r\n';
          } else if (transceiver.rtpSender && transceiver.rtpReceiver) {
            sdp += 'a=sendrecv\r\n';
          } else if (transceiver.rtpSender) {
            sdp += 'a=sendonly\r\n';
          } else if (transceiver.rtpReceiver) {
            sdp += 'a=recvonly\r\n';
          } else {
            sdp += 'a=inactive\r\n';
          }

          if (transceiver.rtpSender) {
            // spec.
            let msid = 'msid:' + stream.id + ' ' +
              transceiver.rtpSender.track.id + '\r\n';
            sdp += 'a=' + msid;

            // for Chrome.
            sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
              ' ' + msid;
            if (transceiver.sendEncodingParameters[0].rtx) {
              sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
                ' ' + msid;
              sdp += 'a=ssrc-group:FID ' +
                transceiver.sendEncodingParameters[0].ssrc + ' ' +
                transceiver.sendEncodingParameters[0].rtx.ssrc +
                '\r\n';
            }
          }
          // FIX ME: this should be written by writeRtpDescription.
          sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
            ' cname:' + SDPUtils.localCName + '\r\n';
          if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
            sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
              ' cname:' + SDPUtils.localCName + '\r\n';
          }
          return sdp;
        };

// Gets the direction from the mediaSection or the sessionpart.
        SDPUtils.getDirection = function (mediaSection, sessionpart) {
          // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv.
          let lines = SDPUtils.splitLines(mediaSection);
          for (let i = 0; i < lines.length; i++) {
            switch (lines[i]) {
              case 'a=sendrecv':
              case 'a=sendonly':
              case 'a=recvonly':
              case 'a=inactive':
                return lines[i].substr(2);
              default:
              // FIX ME: What should happen here?
            }
          }
          if (sessionpart) {
            return SDPUtils.getDirection(sessionpart);
          }
          return 'sendrecv';
        };

        SDPUtils.getKind = function (mediaSection) {
          let lines = SDPUtils.splitLines(mediaSection);
          let mline = lines[0].split(' ');
          return mline[0].substr(2);
        };

        SDPUtils.isRejected = function (mediaSection) {
          return mediaSection.split(' ', 2)[1] === '0';
        };

        SDPUtils.parseMLine = function (mediaSection) {
          let lines = SDPUtils.splitLines(mediaSection);
          let parts = lines[0].substr(2).split(' ');
          return {
            kind: parts[0],
            port: parseInt(parts[1], 10),
            protocol: parts[2],
            fmt: parts.slice(3).join(' ')
          };
        };

        SDPUtils.parseOLine = function (mediaSection) {
          let line = SDPUtils.matchPrefix(mediaSection, 'o=')[0];
          let parts = line.substr(2).split(' ');
          return {
            username: parts[0],
            sessionId: parts[1],
            sessionVersion: parseInt(parts[2], 10),
            netType: parts[3],
            addressType: parts[4],
            address: parts[5]
          };
        };

// a very naive interpretation of a valid SDP.
        SDPUtils.isValidSDP = function (blob) {
          if (typeof blob !== 'string' || blob.length === 0) {
            return false;
          }
          let lines = SDPUtils.splitLines(blob);
          for (let i = 0; i < lines.length; i++) {
            if (lines[i].length < 2 || lines[i].charAt(1) !== '=') {
              return false;
            }
            // TO DO: check the modifier a bit more.
          }
          return true;
        };

// Expose public methods.
        if (typeof module === 'object') {
          module.exports = SDPUtils;
        }

      }, {}]
    }, {}, [1])(1)
  });

</script>
<script title="main.js" type="text/babel">
  /*
 *  Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */

  'use strict';

  const addButton = document.querySelector('button#add');
  const candidateTBody = document.querySelector('tbody#candidatesBody');
  const gatherButton = document.querySelector('button#gather');
  const passwordInput = document.querySelector('input#password');
  const removeButton = document.querySelector('button#remove');
  const resetButton = document.querySelector('button#reset');
  const servers = document.querySelector('select#servers');
  const urlInput = document.querySelector('input#url');
  const usernameInput = document.querySelector('input#username');
  const iceCandidatePoolInput = document.querySelector('input#iceCandidatePool');

  addButton.onclick = addServer;
  gatherButton.onclick = start;
  removeButton.onclick = removeServer;
  resetButton.onclick = () => {
    window.localStorage.clear();
    document.querySelectorAll('select#servers option').forEach(option => option.remove());
    const serversSelect = document.querySelector('select#servers');
    setDefaultServer(serversSelect);
  };

  iceCandidatePoolInput.onchange = function (e) {
    const span = e.target.parentElement.querySelector('span');
    span.textContent = e.target.value;
  };

  let begin;
  let pc;
  let candidates;

  const allServersKey = 'servers';

  function setDefaultServer(serversSelect) {
    const o = document.createElement('option');
    //o.value = '{"urls":["stun:stun.l.google.com:19302"]}';
    o.value = '{"urls":["stun:b.7mo.org:3478"]}';
    // o.text = 'stun:stun.l.google.com:19302';
    o.text = 'stun:b.7mo.org:3478';
    serversSelect.add(o);
  }

  function writeServersToLocalStorage() {
    const serversSelect = document.querySelector('select#servers');
    const allServers = JSON.stringify(Object.values(serversSelect.options).map(o => JSON.parse(o.value)));
    window.localStorage.setItem(allServersKey, allServers);
  }

  function readServersFromLocalStorage() {
    document.querySelectorAll('select#servers option').forEach(option => option.remove());
    const serversSelect = document.querySelector('select#servers');
    const storedServers = window.localStorage.getItem(allServersKey);

    if (storedServers === null || storedServers === '') {
      setDefaultServer(serversSelect);
    } else {
      JSON.parse(storedServers).forEach((server) => {
        const o = document.createElement('option');
        o.value = JSON.stringify(server);
        o.text = server.urls[0];
        o.ondblclick = selectServer;
        serversSelect.add(o);
      });
    }
  }

  function selectServer(event) {
    const option = event.target;
    const value = JSON.parse(option.value);
    urlInput.value = value.urls[0];
    usernameInput.value = value.username || '';
    passwordInput.value = value.credential || '';
  }

  function addServer() {
    const scheme = urlInput.value.split(':')[0];
    if (scheme !== 'stun' && scheme !== 'turn' && scheme !== 'turns') {
      alert(`URI scheme ${scheme} is not valid`);
      return;
    }

    // Store the ICE server as a stringified JSON object in option.value.
    const option = document.createElement('option');
    const iceServer = {
      urls: [urlInput.value],
      username: usernameInput.value,
      credential: passwordInput.value
    };
    option.value = JSON.stringify(iceServer);
    option.text = `${urlInput.value} `;
    const username = usernameInput.value;
    const password = passwordInput.value;
    if (username || password) {
      option.text += (` [${username}:${password}]`);
    }
    option.ondblclick = selectServer;
    servers.add(option);
    urlInput.value = usernameInput.value = passwordInput.value = '';
    writeServersToLocalStorage();
  }

  function removeServer() {
    for (let i = servers.options.length - 1; i >= 0; --i) {
      if (servers.options[i].selected) {
        servers.remove(i);
      }
    }
    writeServersToLocalStorage();
  }

  function start() {
    // Clean out the table.
    while (candidateTBody.firstChild) {
      candidateTBody.removeChild(candidateTBody.firstChild);
    }

    gatherButton.disabled = true;

    // Read the values from the input boxes.
    const iceServers = [];
    for (let i = 0; i < servers.length; ++i) {
      iceServers.push(JSON.parse(servers[i].value));
    }
    const transports = document.getElementsByName('transports');
    let iceTransports;
    for (let i = 0; i < transports.length; ++i) {
      if (transports[i].checked) {
        iceTransports = transports[i].value;
        break;
      }
    }

    // Create a PeerConnection with no streams, but force a m=audio line.
    const config = {
      iceServers: iceServers,
      iceTransportPolicy: iceTransports,
      iceCandidatePoolSize: iceCandidatePoolInput.value
    };

    const offerOptions = {offerToReceiveAudio: 1};
    // Whether we gather IPv6 candidates.
    // Whether we only gather a single set of candidates for RTP and RTCP.

    console.log(`Creating new PeerConnection with config=${JSON.stringify(config)}`);
    document.getElementById('error').innerText = '';
    pc = new RTCPeerConnection(config);
    pc.onicecandidate = iceCallback;
    pc.onicegatheringstatechange = gatheringStateChange;
    pc.onicecandidateerror = iceCandidateError;
    pc.createOffer(
      offerOptions
    ).then(
      gotDescription,
      noDescription
    );
  }

  function gotDescription(desc) {
    begin = window.performance.now();
    candidates = [];
    pc.setLocalDescription(desc);
  }

  function noDescription(error) {
    console.log('Error creating offer: ', error);
  }

  // Parse the uint32 PRIORITY field into its constituent parts from RFC 5245,
  // type preference, local preference, and (256 - component ID).
  // ex: 126 | 32252 | 255 (126 is host preference, 255 is component ID 1)
  function formatPriority(priority) {
    return [
      priority >> 24,
      (priority >> 8) & 0xFFFF,
      priority & 0xFF
    ].join(' | ');
  }

  function appendCell(row, val, span) {
    const cell = document.createElement('td');
    cell.textContent = val;
    if (span) {
      cell.setAttribute('colspan', span);
    }
    row.appendChild(cell);
  }

  // Try to determine authentication failures and unreachable TURN
  // servers by using heuristics on the candidate types gathered.
  function getFinalResult() {
    let result = 'Done';

    // if more than one server is used, it can not be determined
    // which server failed.
    if (servers.length === 1) {
      const server = JSON.parse(servers[0].value);

      // get the candidates types (host, srflx, relay)
      const types = candidates.map(function (cand) {
        return cand.type;
      });

      // If the server is a TURN server we should have a relay candidate.
      // If we did not get a relay candidate but a srflx candidate
      // authentication might have failed.
      // If we did not get  a relay candidate or a srflx candidate
      // we could not reach the TURN server. Either it is not running at
      // the target address or the clients access to the port is blocked.
      //
      // This only works for TURN/UDP since we do not get
      // srflx candidates from TURN/TCP.
      if (server.urls[0].indexOf('turn:') === 0 &&
        server.urls[0].indexOf('?transport=tcp') === -1) {
        if (types.indexOf('relay') === -1) {
          if (types.indexOf('srflx') > -1) {
            // a binding response but no relay candidate suggests auth failure.
            result = 'Authentication failed?';
          } else {
            // either the TURN server is down or the clients access is blocked.
            result = 'Not reachable?';
          }
        }
      }
    }
    return result;
  }

  function iceCallback(event) {
    const elapsed = ((window.performance.now() - begin) / 1000).toFixed(3);
    const row = document.createElement('tr');
    appendCell(row, elapsed);
    if (event.candidate) {
      if (event.candidate.candidate === '') {
        return;
      }
      const {candidate} = event;
      appendCell(row, candidate.component);
      appendCell(row, candidate.type);
      appendCell(row, candidate.foundation);
      appendCell(row, candidate.protocol);
      appendCell(row, candidate.address);
      appendCell(row, candidate.port);
      appendCell(row, formatPriority(candidate.priority));
      candidates.push(candidate);
    } else if (!('onicegatheringstatechange' in RTCPeerConnection.prototype)) {
      // should not be done if its done in the icegatheringstatechange callback.
      appendCell(row, getFinalResult(), 7);
      pc.close();
      pc = null;
      gatherButton.disabled = false;
    }
    candidateTBody.appendChild(row);
  }

  function gatheringStateChange() {
    if (pc.iceGatheringState !== 'complete') {
      return;
    }
    const elapsed = ((window.performance.now() - begin) / 1000).toFixed(3);
    const row = document.createElement('tr');
    appendCell(row, elapsed);
    appendCell(row, getFinalResult(), 7);
    pc.close();
    pc = null;
    gatherButton.disabled = false;
    candidateTBody.appendChild(row);
  }

  function iceCandidateError(e) {
    // The interesting attributes of the error are
    // * the url (which allows looking up the server)
    // * the errorCode and errorText
    document.getElementById('error-note').style.display = 'block';
    document.getElementById('error').innerText += 'The server ' + e.url +
      ' returned an error with code=' + e.errorCode + ':\n' +
      e.errorText + '\n';
  }

  readServersFromLocalStorage();

  // check if we have getUserMedia permissions.
  navigator.mediaDevices
    .enumerateDevices()
    .then(function (devices) {
      devices.forEach(function (device) {
        if (device.label !== '') {
          document.getElementById('getUserMediaPermissions').style.display = 'block';
        }
      });
    });

</script>
<script title="ga.js" type="text/babel">
  /*(function(i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject']=r; i[r]=i[r]||function() {
  (i[r].q=i[r].q||[]).push(arguments);
}, i[r].l=1*new Date(); a=s.createElement(o),
  m=s.getElementsByTagName(o)[0]; a.async=1; a.src=g; m.parentNode.insertBefore(a, m);
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');

ga('create', 'UA-48530561-1', 'auto');
ga('send', 'pageview');
*/
</script>
</body>
</html>
